├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── github-pull-reqeust.yml │ └── github-release.yml ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── bambu ├── frontend │ ├── index.html │ └── themes │ │ └── bambu-theme │ │ ├── bambu.css │ │ ├── components │ │ └── vaadin-text-field.css │ │ ├── styles.css │ │ └── theme-editor.css ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── tfyre │ │ ├── bambu │ │ ├── AppConfig.java │ │ ├── BambuConfig.java │ │ ├── CloudApi.java │ │ ├── CloudService.java │ │ ├── MainLayout.java │ │ ├── PrinterModelConverter.java │ │ ├── SystemRoles.java │ │ ├── YesNoCancelDialog.java │ │ ├── camel │ │ │ └── CamelController.java │ │ ├── printer │ │ │ ├── BambuConst.java │ │ │ ├── BambuErrors.java │ │ │ ├── BambuPrinter.java │ │ │ ├── BambuPrinterConsumer.java │ │ │ ├── BambuPrinterException.java │ │ │ ├── BambuPrinterImpl.java │ │ │ ├── BambuPrinterStream.java │ │ │ ├── BambuPrinters.java │ │ │ ├── BambuPrintersImpl.java │ │ │ ├── Filament.java │ │ │ └── FilamentType.java │ │ ├── security │ │ │ ├── NavigationAccessCheckerInitializer.java │ │ │ └── SecurityUtils.java │ │ └── view │ │ │ ├── FilamentView.java │ │ │ ├── GCodeDialog.java │ │ │ ├── GridHelper.java │ │ │ ├── LoginView.java │ │ │ ├── LogsView.java │ │ │ ├── MaintenanceView.java │ │ │ ├── NotificationHelper.java │ │ │ ├── PrinterView.java │ │ │ ├── PushDiv.java │ │ │ ├── SdCardView.java │ │ │ ├── UpdateHeader.java │ │ │ ├── ViewHelper.java │ │ │ ├── batchprint │ │ │ ├── BatchPrintView.java │ │ │ ├── FilamentHelper.java │ │ │ ├── Plate.java │ │ │ ├── PlateFilament.java │ │ │ ├── PrinterMapping.java │ │ │ ├── ProjectException.java │ │ │ └── ProjectFile.java │ │ │ └── dashboard │ │ │ ├── Dashboard.java │ │ │ └── DashboardPrinter.java │ │ ├── ftp │ │ ├── BambuFtp.java │ │ ├── FTPEventListener.java │ │ └── FTPSClientProvider.java │ │ └── servlet │ │ ├── TFyreIdentityManager.java │ │ ├── TFyreIdentityProvider.java │ │ └── TFyreServletExtension.java │ └── resources │ ├── META-INF │ ├── beans.xml │ ├── resources │ │ └── bambu │ │ │ ├── README.md │ │ │ ├── ams_humidity_0.svg │ │ │ ├── ams_humidity_1.svg │ │ │ ├── ams_humidity_2.svg │ │ │ ├── ams_humidity_3.svg │ │ │ ├── ams_humidity_4.svg │ │ │ ├── fetchAll.sh │ │ │ ├── monitor_bed_temp.svg │ │ │ ├── monitor_bed_temp_active.svg │ │ │ ├── monitor_frame_temp.svg │ │ │ ├── monitor_lamp_off.svg │ │ │ ├── monitor_lamp_on.svg │ │ │ ├── monitor_nozzle_temp.svg │ │ │ ├── monitor_nozzle_temp_active.svg │ │ │ ├── monitor_speed.svg │ │ │ └── monitor_speed_active.svg │ └── services │ │ ├── com.vaadin.flow.server.VaadinServiceInitListener │ │ └── io.undertow.servlet.ServletExtension │ └── application.properties ├── common ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── tfyre │ │ └── bambu │ │ ├── mqtt │ │ └── AbstractMqttController.java │ │ └── ssl │ │ └── NoopTrustSocketFactory.java │ ├── proto │ └── bambu.proto │ ├── resources │ └── META-INF │ │ └── beans.xml │ └── schema │ ├── catalog.xml │ ├── schema.xjb │ ├── slice-info-1.0.xsd │ └── xml.xsd ├── docker ├── bambu-liveview │ ├── README.md │ ├── common-liveview.yml │ ├── example - compose.yml │ ├── example - mediamtx.yml │ └── reverse-proxy.conf └── bambu-local-dev │ ├── README.md │ ├── compose.yaml │ ├── mqtt │ ├── mosquitto.conf │ └── password.conf │ └── vsftpd │ ├── Dockerfile │ ├── run-vsftpd.sh │ ├── users.txt │ ├── vsftpd.conf │ └── vsftpd_virtual ├── docs ├── README.service.md ├── bambufarm1.jpg ├── bambufarm2.jpg ├── bambufarm3.jpg └── batchprint.png ├── mvnw ├── mvnw.cmd ├── pom.xml └── server ├── pom.xml └── src └── main ├── java └── com │ └── tfyre │ └── bambu │ └── server │ ├── BambuConfig.java │ ├── BambuPrinterProcessor.java │ └── CamelController.java └── resources ├── META-INF └── beans.xml ├── application.properties └── json ├── fullstatus.json └── status.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: tfyre 2 | ko_fi: tfyre 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "maven" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/github-pull-reqeust.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Maven and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path 3 | 4 | name: Github Pull Request 5 | 6 | on: 7 | pull_request: 8 | branches: [ main] 9 | push: 10 | branches: [ main] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up JDK 21 22 | uses: actions/setup-java@v4 23 | with: 24 | java-version: '21' 25 | distribution: 'zulu' 26 | server-id: github 27 | settings-path: ${{ github.workspace }} # location for the settings.xml file 28 | 29 | - name: Build with Maven 30 | run: ./mvnw -B package -Pproduction --file pom.xml 31 | 32 | - name: Run tests 33 | run: ./mvnw -B test --file pom.xml -------------------------------------------------------------------------------- /.github/workflows/github-release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Maven and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path 3 | 4 | name: Github Release 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up JDK 21 20 | uses: actions/setup-java@v4 21 | with: 22 | java-version: '21' 23 | distribution: 'zulu' 24 | server-id: github 25 | settings-path: ${{ github.workspace }} # location for the settings.xml file 26 | 27 | - name: Build with Maven 28 | run: ./mvnw -B package -Pproduction --file pom.xml 29 | 30 | - name: Release 31 | uses: softprops/action-gh-release@v1 32 | if: startsWith(github.ref, 'refs/tags/') 33 | with: 34 | files: bambu/target/bambu-web-*-runner.jar 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | nbactions.xml 8 | nb-configuration.xml 9 | .DS_Store 10 | .idea 11 | 12 | # The following files are generated/updated by vaadin-maven-plugin 13 | bambu/node_modules/ 14 | bambu/frontend/generated/ 15 | bambu/.flow-node-tasks.lock 16 | bambu/styles.css 17 | .env 18 | 19 | /docker/bambu-local-dev/vsftpd/home/ 20 | /docker/bambu-local-dev/*.crt 21 | /docker/bambu-local-dev/*.key 22 | /docker/bambu-local-dev/*.srl 23 | /docker/bambu-local-dev/*.csr 24 | 25 | /docker/bambu-liveview/bambu-web-* 26 | /docker/bambu-liveview/compose.yml 27 | /docker/bambu-liveview/mediamtx.yml 28 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TFyre/bambu-farm/c788104297e79502aa14825572b9ecc3762676c4/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.1/apache-maven-3.9.1-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | REPOSITORY OWNER. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /bambu/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /bambu/frontend/themes/bambu-theme/components/vaadin-text-field.css: -------------------------------------------------------------------------------- 1 | /* 2 | CSS styling examples for the Vaadin app. 3 | Visit https://vaadin.com/docs/flow/theme/theming-overview.html and 4 | https://vaadin.com/themes/lumo for more information. 5 | */ 6 | 7 | /* Example: the style is applied only to the textfields which has the `bordered` theme attribute. */ 8 | :host([theme~="bordered"]) [part="input-field"] { 9 | box-shadow: inset 0 0 0 1px var(--lumo-contrast-30pct); 10 | background-color: var(--lumo-base-color); 11 | } 12 | -------------------------------------------------------------------------------- /bambu/frontend/themes/bambu-theme/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | CSS styling examples for the Vaadin app. 3 | Visit https://vaadin.com/docs/flow/theme/theming-overview.html and 4 | https://vaadin.com/themes/lumo for more information. 5 | */ 6 | 7 | @import 'bambu.css'; 8 | 9 | /* Example: CSS class name to center align the content . */ 10 | .centered-content { 11 | margin: 0 auto; 12 | max-width: 100%; 13 | } 14 | 15 | a[highlight] { 16 | font-weight: bold; 17 | text-decoration: underline; 18 | } 19 | -------------------------------------------------------------------------------- /bambu/frontend/themes/bambu-theme/theme-editor.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TFyre/bambu-farm/c788104297e79502aa14825572b9ecc3762676c4/bambu/frontend/themes/bambu-theme/theme-editor.css -------------------------------------------------------------------------------- /bambu/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.tfyre.bambu 6 | bambu-parent 7 | 1.7.0 8 | 9 | bambu-web 10 | jar 11 | 12 | false 13 | 14 | 15 | 16 | 17 | ${project.groupId} 18 | bambu-common 19 | ${project.version} 20 | 21 | 22 | io.quarkus 23 | quarkus-grpc 24 | 25 | 26 | 27 | 28 | 29 | com.vaadin 30 | vaadin-quarkus-extension 31 | 32 | 33 | com.vaadin 34 | vaadin-notification-flow 35 | 36 | 37 | io.quarkus 38 | quarkus-scheduler 39 | 40 | 41 | io.quarkus 42 | quarkus-rest-jackson 43 | 44 | 45 | io.quarkus 46 | quarkus-rest-client-jackson 47 | 48 | 49 | io.quarkus 50 | quarkus-security 51 | 52 | 53 | io.quarkus 54 | quarkus-elytron-security-common 55 | 56 | 57 | 58 | io.quarkus 59 | quarkus-jaxb 60 | 61 | 62 | 63 | commons-net 64 | commons-net 65 | 66 | 67 | 68 | 69 | net.java.dev.jna 70 | jna 71 | 72 | 73 | net.java.dev.jna 74 | jna-platform 75 | 76 | 77 | 78 | org.bouncycastle 79 | bctls-jdk18on 80 | 81 | 82 | 83 | package quarkus:dev 84 | 85 | 86 | io.quarkus 87 | quarkus-maven-plugin 88 | ${quarkus.version} 89 | true 90 | 91 | 92 | 93 | build 94 | generate-code 95 | generate-code-tests 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | production 107 | 108 | 109 | 110 | com.vaadin 111 | vaadin-maven-plugin 112 | ${vaadin.version} 113 | 114 | 115 | 116 | prepare-frontend 117 | build-frontend 118 | 119 | compile 120 | 121 | 122 | 123 | 124 | 125 | 126 | uber-jar 127 | 128 | 129 | 130 | native 131 | 132 | 133 | native 134 | 135 | 136 | 137 | 138 | 139 | maven-failsafe-plugin 140 | 141 | 142 | 143 | integration-test 144 | verify 145 | 146 | 147 | 148 | ${project.build.directory}/${project.build.finalName}-runner 149 | org.jboss.logmanager.LogManager 150 | ${maven.home} 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | true 160 | true 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/AppConfig.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu; 2 | 3 | import com.vaadin.flow.component.page.AppShellConfigurator; 4 | import com.vaadin.flow.component.page.Inline; 5 | import com.vaadin.flow.component.page.Push; 6 | import com.vaadin.flow.server.AppShellSettings; 7 | import com.vaadin.flow.theme.Theme; 8 | import io.quarkus.logging.Log; 9 | import java.io.IOException; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.nio.file.Paths; 13 | 14 | /** 15 | * 16 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 17 | */ 18 | @Theme(value = "bambu-theme") 19 | @Push 20 | public class AppConfig implements AppShellConfigurator { 21 | 22 | private static final String STYLES = "styles.css"; 23 | 24 | @Override 25 | public void configurePage(final AppShellSettings settings) { 26 | settings.setPageTitle("Bambu Web Interface"); 27 | final Path styleSheet = Paths.get(STYLES); 28 | if (Files.isRegularFile(styleSheet)) { 29 | try { 30 | final String contents = new String(Files.readAllBytes(styleSheet)); 31 | settings.addInlineWithContents(contents, Inline.Wrapping.STYLESHEET); 32 | } catch (IOException ex) { 33 | Log.errorf(ex, "Error loading %s - %s", STYLES, ex.getMessage()); 34 | } 35 | } 36 | //FIXME: secure /q/metrics 37 | 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/BambuConfig.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu; 2 | 3 | import com.tfyre.bambu.printer.BambuConst.PrinterModel; 4 | import io.smallrye.config.ConfigMapping; 5 | import io.smallrye.config.WithConverter; 6 | import io.smallrye.config.WithDefault; 7 | import java.time.Duration; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | 12 | /** 13 | * 14 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 15 | */ 16 | @ConfigMapping(prefix = "bambu") 17 | public interface BambuConfig { 18 | 19 | @WithDefault("false") 20 | boolean useBouncyCastle(); 21 | 22 | @WithDefault("true") 23 | boolean menuLeftClick(); 24 | 25 | @WithDefault("false") 26 | boolean darkMode(); 27 | 28 | @WithDefault("5000") 29 | int moveXy(); 30 | 31 | @WithDefault("3000") 32 | int moveZ(); 33 | 34 | @WithDefault("1s") 35 | Duration refreshInterval(); 36 | 37 | @WithDefault("true") 38 | boolean remoteView(); 39 | 40 | Optional liveViewUrl(); 41 | 42 | Dashboard dashboard(); 43 | 44 | BatchPrint batchPrint(); 45 | 46 | Map printers(); 47 | 48 | @WithDefault("false") 49 | boolean autoLogin(); 50 | 51 | Map users(); 52 | 53 | Optional> preheat(); 54 | 55 | Cloud cloud(); 56 | 57 | public interface BatchPrint { 58 | 59 | @WithDefault("true") 60 | boolean skipSameSize(); 61 | 62 | @WithDefault("true") 63 | boolean timelapse(); 64 | 65 | @WithDefault("true") 66 | boolean bedLevelling(); 67 | 68 | @WithDefault("true") 69 | boolean flowCalibration(); 70 | 71 | @WithDefault("true") 72 | boolean vibrationCalibration(); 73 | 74 | @WithDefault("true") 75 | boolean enforceFilamentMapping(); 76 | 77 | } 78 | 79 | public interface Cloud { 80 | 81 | @WithDefault("false") 82 | boolean enabled(); 83 | 84 | @WithDefault("ssl://us.mqtt.bambulab.com:8883") 85 | String url(); 86 | 87 | Optional username(); 88 | 89 | Optional token(); 90 | 91 | } 92 | 93 | public interface Dashboard { 94 | 95 | @WithDefault("true") 96 | boolean remoteView(); 97 | 98 | @WithDefault("true") 99 | boolean filamentFullName(); 100 | 101 | } 102 | 103 | public interface Printer { 104 | 105 | @WithDefault("true") 106 | boolean enabled(); 107 | 108 | Optional name(); 109 | 110 | String deviceId(); 111 | 112 | @WithDefault("bblp") 113 | String username(); 114 | 115 | String accessCode(); 116 | 117 | String ip(); 118 | 119 | @WithDefault("true") 120 | boolean useAms(); 121 | 122 | @WithDefault("true") 123 | boolean timelapse(); 124 | 125 | @WithDefault("true") 126 | boolean bedLevelling(); 127 | 128 | @WithDefault("true") 129 | boolean flowCalibration(); 130 | 131 | @WithDefault("true") 132 | boolean vibrationCalibration(); 133 | 134 | Mqtt mqtt(); 135 | 136 | Ftp ftp(); 137 | 138 | Stream stream(); 139 | 140 | @WithDefault("unknown") 141 | @WithConverter(PrinterModelConverter.class) 142 | PrinterModel model(); 143 | 144 | public interface Mqtt { 145 | 146 | @WithDefault("8883") 147 | int port(); 148 | 149 | Optional url(); 150 | 151 | Optional reportTopic(); 152 | 153 | Optional requestTopic(); 154 | 155 | @WithDefault("10m") 156 | Duration fullStatus(); 157 | 158 | } 159 | 160 | public interface Ftp { 161 | 162 | @WithDefault("990") 163 | int port(); 164 | 165 | Optional url(); 166 | 167 | @WithDefault("false") 168 | boolean logCommands(); 169 | 170 | } 171 | 172 | public interface Stream { 173 | 174 | @WithDefault("true") 175 | boolean enabled(); 176 | 177 | @WithDefault("6000") 178 | int port(); 179 | 180 | @WithDefault("false") 181 | boolean liveView(); 182 | 183 | Optional url(); 184 | 185 | @WithDefault("5m") 186 | Duration watchDog(); 187 | } 188 | } 189 | 190 | public interface User { 191 | 192 | String password(); 193 | 194 | String role(); 195 | 196 | Optional darkMode(); 197 | 198 | } 199 | 200 | public interface Temperature { 201 | 202 | String name(); 203 | 204 | int bed(); 205 | 206 | int nozzle(); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/CloudApi.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu; 2 | 3 | import io.smallrye.mutiny.Uni; 4 | import jakarta.ws.rs.Consumes; 5 | import jakarta.ws.rs.POST; 6 | import jakarta.ws.rs.Path; 7 | import jakarta.ws.rs.core.MediaType; 8 | import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; 9 | import org.jboss.resteasy.reactive.RestResponse; 10 | 11 | /** 12 | * 13 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 14 | */ 15 | @RegisterRestClient(configKey = "cloud") 16 | public interface CloudApi { 17 | 18 | @Consumes(MediaType.APPLICATION_JSON) 19 | @POST 20 | @Path("api/sign-in/form") 21 | Uni> login(final Login login); 22 | 23 | record Login(String account, String password) { 24 | 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/CloudService.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu; 2 | 3 | import io.quarkus.logging.Log; 4 | import jakarta.inject.Inject; 5 | import jakarta.inject.Singleton; 6 | import java.util.Optional; 7 | import org.eclipse.microprofile.rest.client.inject.RestClient; 8 | import org.jboss.resteasy.reactive.RestResponse; 9 | 10 | /** 11 | * 12 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 13 | */ 14 | @Singleton 15 | public class CloudService { 16 | 17 | @Inject 18 | BambuConfig config; 19 | 20 | @RestClient 21 | CloudApi api; 22 | 23 | public Optional getLoginData() { 24 | if (!config.cloud().enabled()) { 25 | return Optional.empty(); 26 | } 27 | if (config.cloud().username().isEmpty()) { 28 | Log.error("bambu.cloud.username should be configured, cannot enable cloud mode"); 29 | return Optional.empty(); 30 | } 31 | if (config.cloud().token().isEmpty()) { 32 | Log.error("bambu.cloud.token should be configured, cannot enable cloud mode"); 33 | return Optional.empty(); 34 | } 35 | Log.info("username & token login"); 36 | return Optional.of(new Data(config.cloud().username().get(), config.cloud().token().get())); 37 | } 38 | 39 | public record Data(String username, String password) { 40 | 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/PrinterModelConverter.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu; 2 | 3 | import com.tfyre.bambu.printer.BambuConst; 4 | import org.eclipse.microprofile.config.spi.Converter; 5 | 6 | /** 7 | * 8 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 9 | */ 10 | public class PrinterModelConverter implements Converter { 11 | 12 | @Override 13 | public BambuConst.PrinterModel convert(final String value) throws IllegalArgumentException, NullPointerException { 14 | return BambuConst.PrinterModel.fromModel(value) 15 | .orElseThrow(() -> new IllegalArgumentException("[%s] cannot be converted to PrinterModel".formatted(value))); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/SystemRoles.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu; 2 | 3 | /** 4 | * 5 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 6 | */ 7 | public class SystemRoles { 8 | 9 | public static final String USER_ADMIN = "admin"; 10 | public static final String ROLE_ADMIN = "admin"; 11 | public static final String ROLE_NORMAL = "normal"; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/YesNoCancelDialog.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu; 2 | 3 | import com.vaadin.flow.component.Component; 4 | import com.vaadin.flow.component.button.Button; 5 | import com.vaadin.flow.component.button.ButtonVariant; 6 | import com.vaadin.flow.component.dialog.Dialog; 7 | import com.vaadin.flow.component.html.Span; 8 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.function.Consumer; 12 | 13 | /** 14 | * 15 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 16 | */ 17 | public class YesNoCancelDialog { 18 | 19 | private static final String HEADER = "Confirmation"; 20 | private static final String TEXT_CONFIRM = "Yes"; 21 | private static final String TEXT_REJECT = "No"; 22 | private static final String TEXT_CANCEL = "Cancel"; 23 | 24 | private boolean confirmed; 25 | private boolean rejected; 26 | private boolean canceled; 27 | 28 | private final Consumer consumer; 29 | private final Dialog dialog = new Dialog(); 30 | 31 | private YesNoCancelDialog(final List extra, final String message, final Consumer consumer) { 32 | this.consumer = consumer; 33 | dialog.setHeaderTitle(HEADER); 34 | dialog.add(new VerticalLayout( 35 | Arrays.asList(message.split("\n")) 36 | .stream() 37 | .map(Span::new) 38 | .toArray(Span[]::new))); 39 | extra.forEach(dialog::add); 40 | 41 | dialog.getFooter().add(getCancel(), getNo(), getYes()); 42 | } 43 | 44 | public static void show(final String message, final Consumer consumer) { 45 | new YesNoCancelDialog(List.of(), message, consumer).open(); 46 | } 47 | 48 | public static void show(final List extra, final String message, final Consumer consumer) { 49 | new YesNoCancelDialog(extra, message, consumer).open(); 50 | } 51 | 52 | private Button getCancel() { 53 | final Button result = new Button(TEXT_CANCEL, e -> onCancel()); 54 | result.addThemeVariants(ButtonVariant.LUMO_TERTIARY); 55 | result.getStyle().set("margin-right", "auto"); 56 | return result; 57 | 58 | } 59 | 60 | private Button getNo() { 61 | final Button result = new Button(TEXT_REJECT, e -> onReject()); 62 | result.addThemeVariants(ButtonVariant.LUMO_ERROR); 63 | return result; 64 | } 65 | 66 | private Button getYes() { 67 | final Button result = new Button(TEXT_CONFIRM, e -> onOK()); 68 | result.addThemeVariants(ButtonVariant.LUMO_PRIMARY); 69 | return result; 70 | } 71 | 72 | private void open() { 73 | dialog.open(); 74 | } 75 | 76 | private void onOK() { 77 | dialog.close(); 78 | confirmed = true; 79 | consumer.accept(this); 80 | } 81 | 82 | private void onReject() { 83 | dialog.close(); 84 | rejected = true; 85 | consumer.accept(this); 86 | } 87 | 88 | private void onCancel() { 89 | dialog.close(); 90 | canceled = true; 91 | consumer.accept(this); 92 | } 93 | 94 | public boolean isConfirmed() { 95 | return confirmed; 96 | } 97 | 98 | public boolean isRejected() { 99 | return rejected; 100 | } 101 | 102 | public boolean isCanceled() { 103 | return canceled; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/camel/CamelController.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.camel; 2 | 3 | import com.tfyre.bambu.BambuConfig; 4 | import com.tfyre.bambu.BambuConfig.Printer; 5 | import com.tfyre.bambu.CloudService; 6 | import com.tfyre.bambu.printer.BambuPrinters; 7 | import com.tfyre.bambu.mqtt.AbstractMqttController; 8 | import com.tfyre.bambu.printer.BambuPrinterException; 9 | import io.quarkus.logging.Log; 10 | import io.quarkus.runtime.Startup; 11 | import jakarta.enterprise.context.ApplicationScoped; 12 | import jakarta.inject.Inject; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.Optional; 16 | import java.util.stream.Collectors; 17 | import org.apache.camel.CamelContext; 18 | import org.apache.camel.Endpoint; 19 | import org.apache.camel.StartupListener; 20 | import org.eclipse.microprofile.context.ManagedExecutor; 21 | 22 | /** 23 | * 24 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 25 | */ 26 | @Startup 27 | @ApplicationScoped 28 | public class CamelController extends AbstractMqttController implements StartupListener { 29 | 30 | @Inject 31 | BambuConfig config; 32 | @Inject 33 | CloudService cloud; 34 | 35 | @Inject 36 | BambuPrinters printers; 37 | 38 | @Inject 39 | ManagedExecutor executor; 40 | 41 | Optional cloudData = Optional.empty(); 42 | 43 | @Override 44 | public void onCamelContextStarted(final CamelContext context, final boolean alreadyStarted) throws Exception { 45 | 46 | } 47 | 48 | @Override 49 | public void onCamelContextFullyStarted(final CamelContext context, final boolean alreadyStarted) throws Exception { 50 | executor.submit(() -> { 51 | try { 52 | printers.startPrinters(); 53 | } catch (BambuPrinterException ex) { 54 | Log.errorf(ex, "onCamelContextFullyStarted: %s", ex.getMessage()); 55 | } 56 | }); 57 | } 58 | 59 | private void checkDuplicates() { 60 | final Map> map = config.printers().keySet() 61 | .stream() 62 | .collect(Collectors.groupingBy(String::toLowerCase)); 63 | map.values().forEach(list -> { 64 | if (list.size() < 2) { 65 | return; 66 | } 67 | Log.errorf("!!BROKEN CONFIG!! found duplicate printers: %s", list); 68 | }); 69 | } 70 | 71 | @Override 72 | public void configure() throws Exception { 73 | checkDuplicates(); 74 | getCamelContext().addStartupListener(this); 75 | cloudData = cloud.getLoginData(); 76 | config.printers().forEach(this::configurePrinter); 77 | Log.info("configured"); 78 | } 79 | 80 | private String getUrl(final Printer config) { 81 | return config.mqtt().url().orElseGet(() -> "ssl://%s:%d".formatted(config.ip(), config.mqtt().port())); 82 | } 83 | 84 | private Endpoint getMqttEndpoint(final String topic, final Printer printerConfig) { 85 | return cloudData 86 | .map(data -> getMqttEndpoint(topic, config.cloud().url(), data.username(), data.password())) 87 | .orElseGet(() -> getMqttEndpoint(topic, getUrl(printerConfig), printerConfig.username(), printerConfig.accessCode())); 88 | } 89 | 90 | private void configurePrinter(final String id, final Printer config) { 91 | final String name = config.name().orElse(id); 92 | if (!config.enabled()) { 93 | Log.infof("Skipping: id[%s] as name[%s]", id, name); 94 | return; 95 | } 96 | Log.infof("Configuring: id[%s] as name[%s]", id, name); 97 | final String producerTopic = getTopic(config.mqtt().requestTopic(), config.deviceId(), "request"); 98 | final String consumerTopic = getTopic(config.mqtt().reportTopic(), config.deviceId(), "report"); 99 | final Endpoint producer = getMqttEndpoint(producerTopic, config); 100 | final Endpoint consumer = getMqttEndpoint(consumerTopic, config); 101 | final Endpoint printer = getPrinterEndpoint(name); 102 | 103 | final BambuPrinters.PrinterDetail detail = printers.newPrinter(id, name, config, printer); 104 | 105 | //producer 106 | from(printer) 107 | .id("producer-%s".formatted(name)) 108 | .autoStartup(false) 109 | .group(name) 110 | .to(producer); 111 | //consumer 112 | from(consumer) 113 | .id("consumer-%s".formatted(name)) 114 | .autoStartup(false) 115 | .group(name) 116 | .process(detail.processor()); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/printer/BambuPrinter.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.printer; 2 | 3 | import com.tfyre.bambu.model.BambuMessage; 4 | import com.tfyre.bambu.printer.BambuConst.PrinterModel; 5 | import com.vaadin.flow.server.StreamResource; 6 | import java.time.OffsetDateTime; 7 | import java.util.Collection; 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | /** 12 | * 13 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 14 | */ 15 | public interface BambuPrinter { 16 | 17 | String getName(); 18 | 19 | PrinterModel getModel(); 20 | 21 | int getPrintError(); 22 | 23 | int getTotalLayerNum(); 24 | 25 | BambuConst.GCodeState getGCodeState(); 26 | 27 | Optional getStatus(); 28 | 29 | Optional getFullStatus(); 30 | 31 | Optional getIFrame(); 32 | 33 | Optional getThumbnail(); 34 | 35 | Collection getLastMessages(); 36 | 37 | boolean isBlocked(); 38 | 39 | void setBlocked(final boolean blocked); 40 | 41 | void commandFullStatus(final boolean force); 42 | 43 | void commandClearPrinterError(); 44 | 45 | void commandLight(BambuConst.LightMode lightMode); 46 | 47 | void commandControl(BambuConst.CommandControl control); 48 | 49 | void commandSpeed(BambuConst.Speed speed); 50 | 51 | void commandPrintGCodeLine(final String lines); 52 | 53 | void commandPrintGCodeLine(final List lines); 54 | 55 | void commandPrintGCodeFile(final String filename); 56 | 57 | void commandPrintProjectFile(final CommandPPF command); 58 | 59 | void commandFilamentLoad(final int amsTrayId); 60 | 61 | void commandFilamentUnload(); 62 | 63 | void commandFilamentSetting(final int amsId, final int trayId, final Filament filament, final String color, final int minTemp, final int maxTemp); 64 | 65 | void commandSystemReboot(); 66 | 67 | record Message(OffsetDateTime lastUpdated, BambuMessage message, String raw) { 68 | 69 | } 70 | 71 | record Thumbnail(OffsetDateTime lastUpdated, StreamResource thumbnail) { 72 | 73 | } 74 | 75 | record CommandPPF( 76 | String filename, 77 | int plateId, 78 | boolean useAms, 79 | boolean timelapse, 80 | boolean bedLevelling, 81 | boolean flowCalibration, 82 | boolean vibrationCalibration, 83 | List amsMapping) { 84 | 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/printer/BambuPrinterConsumer.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.printer; 2 | 3 | /** 4 | * 5 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 6 | */ 7 | @FunctionalInterface 8 | public interface BambuPrinterConsumer { 9 | 10 | /** 11 | * Performs this operation on the given argument. 12 | * 13 | * @param t the input argument 14 | */ 15 | void accept(T t) throws BambuPrinterException; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/printer/BambuPrinterException.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.printer; 2 | 3 | /** 4 | * 5 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 6 | */ 7 | public class BambuPrinterException extends Exception { 8 | 9 | public BambuPrinterException() { 10 | } 11 | 12 | public BambuPrinterException(final String message) { 13 | super(message); 14 | } 15 | 16 | public BambuPrinterException(final String message, final Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | public BambuPrinterException(final Throwable cause) { 21 | super(cause); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/printer/BambuPrinterStream.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.printer; 2 | 3 | import com.tfyre.bambu.BambuConfig; 4 | import com.vaadin.flow.server.StreamResource; 5 | import io.netty.buffer.ByteBuf; 6 | import io.netty.buffer.Unpooled; 7 | import io.quarkus.logging.Log; 8 | import io.quarkus.scheduler.Scheduler; 9 | import io.vertx.core.Vertx; 10 | import io.vertx.core.buffer.Buffer; 11 | import io.vertx.core.net.NetClient; 12 | import io.vertx.core.net.NetClientOptions; 13 | import io.vertx.core.net.NetSocket; 14 | import jakarta.enterprise.context.Dependent; 15 | import jakarta.inject.Inject; 16 | import java.io.ByteArrayInputStream; 17 | import java.net.URI; 18 | import java.time.OffsetDateTime; 19 | import java.util.concurrent.ScheduledExecutorService; 20 | import java.util.concurrent.TimeUnit; 21 | import java.util.concurrent.atomic.AtomicBoolean; 22 | import java.util.function.Consumer; 23 | 24 | /** 25 | * 26 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 27 | */ 28 | @Dependent 29 | public class BambuPrinterStream { 30 | 31 | private static final byte[] EMPTY = new byte[32]; 32 | private static final int MAX_SIZE = 10_000_000; 33 | 34 | private final NetClient client; 35 | private NetSocket socket; 36 | 37 | private OffsetDateTime nextImage = OffsetDateTime.now(); 38 | 39 | @Inject 40 | ScheduledExecutorService executor; 41 | 42 | private BambuConfig.Printer config; 43 | private boolean enabled; 44 | private String name; 45 | private Consumer consumer; 46 | 47 | private final AtomicBoolean running = new AtomicBoolean(); 48 | 49 | @Inject 50 | public BambuPrinterStream(final Vertx vertx) { 51 | final NetClientOptions options = new NetClientOptions() 52 | .setHostnameVerificationAlgorithm("") 53 | .setSsl(true) 54 | .setTrustAll(true); 55 | client = vertx.createNetClient(options); 56 | } 57 | 58 | public void setup(final boolean enabled, final Scheduler scheduler, final String name, final BambuConfig.Printer config, final Consumer consumer) { 59 | this.enabled = enabled; 60 | this.name = name; 61 | this.config = config; 62 | this.consumer = consumer; 63 | 64 | if (!enabled) { 65 | return; 66 | } 67 | 68 | scheduler.newJob("%s.checkLastImage#%s".formatted(getClass().getName(), name)) 69 | .setInterval("1m") 70 | .setTask(e -> checkLastImage()) 71 | .schedule(); 72 | } 73 | 74 | private Buffer getHandshake() { 75 | return Buffer.buffer(80) 76 | .appendIntLE(0x40) 77 | .appendIntLE(0x3000) 78 | .appendLong(0) 79 | .appendString(config.username()) 80 | .appendBytes(EMPTY, 0, 32 - config.username().length()) 81 | .appendString(config.accessCode()) 82 | .appendBytes(EMPTY, 0, 32 - config.accessCode().length()); 83 | } 84 | 85 | private URI getURI() { 86 | return URI.create(config.stream().url().orElseGet(() -> "ssl://%s:%d".formatted(config.ip(), config.stream().port()))); 87 | } 88 | 89 | private void startStream() { 90 | final URI uri = getURI(); 91 | client.connect(uri.getPort(), uri.getHost()) 92 | .onSuccess(_s -> { 93 | socket = _s; 94 | final ByteBuf buffer = Unpooled.buffer(0, MAX_SIZE); 95 | socket.handler(h -> { 96 | buffer.writeBytes(h.getBytes()); 97 | Log.debugf("%s: readable %d", name, buffer.readableBytes()); 98 | if (buffer.readableBytes() <= 16) { 99 | return; 100 | } 101 | 102 | buffer.markReaderIndex(); 103 | final int size = buffer.readIntLE(); 104 | Log.debugf("%s: size %d", name, size); 105 | buffer.skipBytes(4 + 8); 106 | if (buffer.readableBytes() < size) { 107 | buffer.resetReaderIndex(); 108 | return; 109 | } 110 | 111 | final byte[] data = new byte[size]; 112 | buffer.readBytes(data).discardReadBytes(); 113 | 114 | consumer.accept(new BambuPrinter.Thumbnail(OffsetDateTime.now(), new StreamResource("image.jpg", () -> new ByteArrayInputStream(data)))); 115 | nextImage = OffsetDateTime.now().plus(config.stream().watchDog()); 116 | }) 117 | .write(getHandshake()) 118 | .onFailure(h -> { 119 | Log.errorf(h, "%s: socketFailure", name); 120 | }); 121 | }) 122 | .onFailure(h -> { 123 | Log.errorf("%s: clientFailure: %s - %s", name, h.getClass().getName(), h.getMessage()); 124 | }); 125 | } 126 | 127 | private void closeSocket() { 128 | if (socket == null) { 129 | return; 130 | } 131 | socket.close(); 132 | socket = null; 133 | } 134 | 135 | public void checkLastImage() { 136 | if (!running.get()) { 137 | return; 138 | } 139 | if (nextImage.isAfter(OffsetDateTime.now())) { 140 | return; 141 | } 142 | Log.errorf("%s: No image received since %s", name, nextImage); 143 | closeSocket(); 144 | executor.schedule(this::startStream, 10, TimeUnit.SECONDS); 145 | } 146 | 147 | public void start() { 148 | if (!enabled) { 149 | return; 150 | } 151 | nextImage = OffsetDateTime.now(); 152 | running.set(true); 153 | startStream(); 154 | } 155 | 156 | public void stop() { 157 | if (!enabled) { 158 | return; 159 | } 160 | running.set(false); 161 | Log.infof("%s: stopping", name); 162 | closeSocket(); 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/printer/BambuPrinters.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.printer; 2 | 3 | import com.tfyre.bambu.BambuConfig; 4 | import java.util.Collection; 5 | import java.util.Optional; 6 | import java.util.concurrent.atomic.AtomicBoolean; 7 | import org.apache.camel.Endpoint; 8 | import org.apache.camel.Processor; 9 | 10 | /** 11 | * 12 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 13 | */ 14 | public interface BambuPrinters { 15 | 16 | Collection getPrinters(); 17 | 18 | Collection getPrintersDetail(); 19 | 20 | Optional getPrinter(final String name); 21 | 22 | Optional getPrinterDetail(final String name); 23 | 24 | PrinterDetail newPrinter(final String id, final String name, final BambuConfig.Printer config, final Endpoint endpoint); 25 | 26 | void startPrinter(final String name) throws BambuPrinterException; 27 | 28 | void stopPrinter(final String name) throws BambuPrinterException; 29 | 30 | void startPrinters() throws BambuPrinterException; 31 | 32 | void stopPrinters() throws BambuPrinterException; 33 | 34 | record PrinterDetail(String id, String name, AtomicBoolean running, BambuConfig.Printer config, BambuPrinter printer, Processor processor, BambuPrinterStream stream) { 35 | 36 | public boolean isRunning() { 37 | return running.get(); 38 | } 39 | 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/printer/BambuPrintersImpl.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.printer; 2 | 3 | import com.tfyre.bambu.BambuConfig; 4 | import io.quarkus.logging.Log; 5 | import io.quarkus.scheduler.Scheduler; 6 | import jakarta.annotation.PreDestroy; 7 | import jakarta.enterprise.context.ApplicationScoped; 8 | import jakarta.enterprise.inject.Instance; 9 | import jakarta.inject.Inject; 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.Collections; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.Optional; 17 | import java.util.concurrent.atomic.AtomicBoolean; 18 | import java.util.function.Consumer; 19 | import org.apache.camel.CamelContext; 20 | import org.apache.camel.Endpoint; 21 | import org.apache.camel.Processor; 22 | import org.apache.camel.Route; 23 | 24 | /** 25 | * 26 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 27 | */ 28 | @ApplicationScoped 29 | public class BambuPrintersImpl implements BambuPrinters { 30 | 31 | @Inject 32 | Instance _bambuPrinter; 33 | @Inject 34 | Instance _bambuPrinterStream; 35 | @Inject 36 | CamelContext camelContext; 37 | @Inject 38 | Scheduler scheduler; 39 | @Inject 40 | BambuConfig bambuConfig; 41 | 42 | private final Map map = new HashMap<>(); 43 | 44 | public BambuPrintersImpl() { 45 | } 46 | 47 | @Override 48 | public Collection getPrinters() { 49 | return map.values().stream() 50 | .filter(PrinterDetail::isRunning) 51 | .map(PrinterDetail::printer) 52 | .toList(); 53 | } 54 | 55 | @Override 56 | public Collection getPrintersDetail() { 57 | return Collections.unmodifiableCollection(map.values()); 58 | } 59 | 60 | private Consumer getConsumer(final String name) { 61 | return o -> { 62 | Log.errorf("%s: no image consumer, BambuPrinter does not extend BambuPrinterImpl", name); 63 | }; 64 | } 65 | 66 | @Override 67 | public PrinterDetail newPrinter(final String id, final String name, final BambuConfig.Printer config, final Endpoint endpoint) { 68 | final BambuPrinter printer = _bambuPrinter.get(); 69 | Consumer consumer = getConsumer(name); 70 | if (printer instanceof BambuPrinterImpl impl) { 71 | impl.setup(scheduler, name, config, endpoint, id); 72 | consumer = impl::setThumbnail; 73 | } 74 | 75 | if (!Processor.class.isInstance(printer)) { 76 | throw new RuntimeException("%s does not implement %s".formatted(printer.getClass().getName(), Processor.class.getName())); 77 | } 78 | 79 | final BambuPrinterStream stream = _bambuPrinterStream.get(); 80 | final boolean enabled = bambuConfig.remoteView() && config.stream().enabled() && !config.stream().liveView(); 81 | stream.setup(enabled, scheduler, name, config, consumer); 82 | 83 | final PrinterDetail result = new PrinterDetail(id, name, new AtomicBoolean(), config, printer, Processor.class.cast(printer), stream); 84 | map.put(name, result); 85 | return result; 86 | } 87 | 88 | private List getRoutes(final PrinterDetail detail) { 89 | return camelContext.getRoutes() 90 | .stream() 91 | .filter(r -> detail.name().equals(r.getGroup())) 92 | .toList(); 93 | } 94 | 95 | private void startPrinter(final PrinterDetail detail) throws BambuPrinterException { 96 | if (detail.isRunning()) { 97 | return; 98 | } 99 | Log.infof("%s: starting", detail.name()); 100 | try { 101 | for (final Route r : getRoutes(detail)) { 102 | try { 103 | camelContext.getRouteController().startRoute(r.getRouteId()); 104 | } catch (Exception ex) { 105 | throw new BambuPrinterException("%s: Error starting route: %s".formatted(detail.name(), r.getRouteId()), ex); 106 | } 107 | } 108 | if (detail.printer() instanceof BambuPrinterImpl impl) { 109 | impl.start(); 110 | } 111 | detail.stream().start(); 112 | detail.running().set(true); 113 | Log.infof("%s: started", detail.name()); 114 | } catch (Throwable t) { 115 | throw new BambuPrinterException("Unknown Exception: %s".formatted(t), t); 116 | } 117 | } 118 | 119 | private void stopPrinter(final PrinterDetail detail) throws BambuPrinterException { 120 | if (!detail.isRunning()) { 121 | return; 122 | } 123 | Log.infof("%s: stopping", detail.name()); 124 | detail.running().set(false); 125 | try { 126 | detail.stream().stop(); 127 | if (detail.printer() instanceof BambuPrinterImpl impl) { 128 | impl.stop(); 129 | } 130 | for (final Route r : getRoutes(detail)) { 131 | try { 132 | camelContext.getRouteController().stopRoute(r.getRouteId()); 133 | } catch (Exception ex) { 134 | throw new BambuPrinterException("%s: Error starting route: %s".formatted(detail.name(), r.getRouteId()), ex); 135 | } 136 | } 137 | Log.infof("%s: stopped", detail.name()); 138 | } catch (Throwable t) { 139 | throw new BambuPrinterException("Unknown Exception: %s".formatted(t), t); 140 | } 141 | } 142 | 143 | @Override 144 | public Optional getPrinter(final String name) { 145 | return Optional.ofNullable(map.get(name)) 146 | .map(PrinterDetail::printer); 147 | } 148 | 149 | @Override 150 | public Optional getPrinterDetail(final String name) { 151 | return Optional.ofNullable(map.get(name)); 152 | } 153 | 154 | private PrinterDetail getPrinterDetailE(final String name) throws BambuPrinterException { 155 | return getPrinterDetail(name).orElseThrow(() -> new BambuPrinterException("%s not found".formatted(name))); 156 | } 157 | 158 | @Override 159 | public void startPrinter(String name) throws BambuPrinterException { 160 | startPrinter(getPrinterDetailE(name)); 161 | } 162 | 163 | @Override 164 | public void stopPrinter(String name) throws BambuPrinterException { 165 | stopPrinter(getPrinterDetailE(name)); 166 | } 167 | 168 | private void stopStart(final BambuPrinterConsumer consumer) throws BambuPrinterException { 169 | final List errors = new ArrayList<>(); 170 | for (final PrinterDetail pd : map.values()) { 171 | try { 172 | consumer.accept(pd); 173 | } catch (BambuPrinterException ex) { 174 | final String message = "%s: %s".formatted(pd.name(), ex.getMessage()); 175 | errors.add(message); 176 | Log.error(message, ex); 177 | } 178 | } 179 | 180 | if (errors.isEmpty()) { 181 | return; 182 | } 183 | throw new BambuPrinterException("Errors with stopStart: %s".formatted(errors)); 184 | } 185 | 186 | @Override 187 | public void startPrinters() throws BambuPrinterException { 188 | stopStart(this::startPrinter); 189 | } 190 | 191 | @Override 192 | public void stopPrinters() throws BambuPrinterException { 193 | stopStart(this::stopPrinter); 194 | } 195 | 196 | @PreDestroy 197 | public void preDestroy() { 198 | Log.info("Stopping Printers"); 199 | try { 200 | stopPrinters(); 201 | } catch (BambuPrinterException ex) { 202 | Log.error(ex); 203 | } 204 | } 205 | 206 | } 207 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/printer/Filament.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.printer; 2 | 3 | import java.util.EnumSet; 4 | import java.util.Map; 5 | import java.util.Optional; 6 | import java.util.function.Function; 7 | import java.util.stream.Collectors; 8 | 9 | /** 10 | * 11 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 12 | */ 13 | public enum Filament { 14 | UNKNOWN("Unknown", "Unknown", FilamentType.UNKNOWN), 15 | BAMBU_ABS("GFB00", "Bambu ABS", FilamentType.ABS), 16 | BAMBU_ASA("GFB01", "Bambu ASA", FilamentType.ASA), 17 | BAMBU_PACF("GFN03", "Bambu PA-CF", FilamentType.PACF), 18 | BAMBU_PA6CF("GFN05", "Bambu PA6-CF", FilamentType.PA6CF), 19 | BAMBU_PAHTCF("GFN04", "Bambu PAHT-CF", FilamentType.PACF), 20 | BAMBU_PC("GFC00", "Bambu PC", FilamentType.PC), 21 | BAMBU_PET_CF("GFT01", "Bambu PET-CF", FilamentType.PETCF), 22 | BAMBU_PETG_BASIC("GFG00", "Bambu PETG Basic", FilamentType.PETG), 23 | BAMBU_PETG_CF("GFG50", "Bambu PETG-CF", FilamentType.PETGCF), 24 | BAMBU_PLA_AERO("GFA11", "Bambu PLA Aero", FilamentType.PLA_AERO), 25 | BAMBU_PLA_BASIC("GFA00", "Bambu PLA Basic", FilamentType.PLA), 26 | BAMBU_PLA_IMPACT("GFA03", "Bambu PLA Impact", FilamentType.PLA), 27 | BAMBU_PLA_MARBLE("GFA07", "Bambu PLA Marble", FilamentType.PLA), 28 | BAMBU_PLA_MATTE("GFA01", "Bambu PLA Matte", FilamentType.PLA), 29 | BAMBU_PLA_METAL("GFA02", "Bambu PLA Metal", FilamentType.PLA), 30 | BAMBU_PLA_SILK("GFA05", "Bambu PLA Silk", FilamentType.PLA), 31 | BAMBU_PLA_SPARKLE("GFA08", "Bambu PLA Sparkle", FilamentType.PLA), 32 | BAMBU_PLA_TOUGH("GFA09", "Bambu PLA Tough", FilamentType.PLA), 33 | BAMBU_PLA_CF("GFA50", "Bambu PLA-CF", FilamentType.PLA_CF), 34 | BAMBU_SUPPORT_PA("GFS03", "Bambu Support For PA/PET", FilamentType.PLA), 35 | BAMBU_SUPPORT_PLA("GFS02", "Bambu Support For PLA", FilamentType.PLA), 36 | BAMBU_SUPPORT_G("GFS01", "Bambu Support G", FilamentType.PLA), 37 | BAMBU_SUPPORT_W("GFS00", "Bambu Support W", FilamentType.PLA), 38 | BAMBU_TPU("GFU01", "Bambu TPU 95A", FilamentType.TPU), 39 | GENERIC_ABS("GFB99", "Generic ABS", FilamentType.ABS), 40 | GENERIC_ASA("GFB98", "Generic ASA", FilamentType.ASA), 41 | GENERIC_HIPS("GFS98", "Generic HIPS", FilamentType.HIPS), 42 | GENERIC_PACF("GFN98", "Generic PA-CF", FilamentType.PACF), 43 | GENERIC_PA("GFN99", "Generic PA", FilamentType.PA), 44 | GENERIC_PC("GFC99", "Generic PC", FilamentType.PC), 45 | GENERIC_PETG("GFG99", "Generic PETG", FilamentType.PETG), 46 | GENERIC_PETC_CF("GFG98", "Generic PETG-CF", FilamentType.PETGCF), 47 | GENERIC_PLA("GFL99", "Generic PLA", FilamentType.PLA), 48 | GENERIC_PLA_HS("GFL95", "Generic PLA-High Speed", FilamentType.PLA), 49 | GENERIC_PLA_SILK("GFL96", "Generic PLA Silk", FilamentType.PLA), 50 | GENERIC_PLA_CF("GFL98", "Generic PLA-CF", FilamentType.PLA_CF), 51 | GENERIC_PVA("GFS99", "Generic PVA", FilamentType.PVA), 52 | GENERIC_TPU("GFU99", "Generic TPU", FilamentType.TPU), 53 | OVERTURE_PLA("GFL05", "Overture Matte PLA", FilamentType.PLA), 54 | OVERTURE_ABS("GFL04", "Overture PLA", FilamentType.PLA), 55 | POLYLITE_ABS("GFB60", "PolyLite ABS", FilamentType.ABS), 56 | POLYLITE_ASA("GFB61", "PolyLite ASA", FilamentType.ASA), 57 | POLYLITE_PETG("GFG60", "PolyLite PETG", FilamentType.PETG), 58 | POLYLITE_PLA("GFL00", "PolyLite PLA", FilamentType.PLA), 59 | POLYTERRA_PLA("GFL01", "PolyTerra PLA", FilamentType.PLA), 60 | ESUN_PLA("GFL03", "eSUN PLA+", FilamentType.PLA), 61 | SUNLU_PLA("GFSNL03", "SUNLU PLA+", FilamentType.PLA), 62 | SUNLU_PLA2("GFSNL04", "SUNLU PLA+ 2.0", FilamentType.PLA), 63 | SUNLU_PLA_MARBLE("GFSNL06", "SUNLU PLA Marble", FilamentType.PLA), 64 | SUNLU_PLA_MATTE("GFSNL02", "SUNLU PLA Matte", FilamentType.PLA), 65 | SUNLU_PLA_SILK("GFSNL05", "SUNLU PLA Silk", FilamentType.PLA), 66 | SUNLU_PLA_WOOD("GFSNL07", "SUNLU PLA Wood", FilamentType.PLA), 67 | SUNLU_PETG("GFSNL08", "SUNLU PETG", FilamentType.PETG), 68 | 69 | 70 | GENERIC_PLA_SLIK_01("GFSL99_01", "Generic PLA Silk 01", FilamentType.PLA), 71 | GENERIC_PLA_SLIK_12("GFSL99_12", "Generic PLA Silk 12", FilamentType.PLA); 72 | 73 | private static final Map MAP = EnumSet.allOf(Filament.class).stream().collect(Collectors.toMap(Filament::getCode, Function.identity())); 74 | private static final Function MAPPER_DESCRIPTION = filament -> filament.getDescription(); 75 | private static final Function MAPPER_TYPE = filament -> filament.getType().getDescription(); 76 | 77 | private final String code; 78 | private final String description; 79 | private final FilamentType type; 80 | 81 | private Filament(final String code, final String description, final FilamentType type) { 82 | this.code = code; 83 | this.description = description; 84 | this.type = type; 85 | } 86 | 87 | public String getCode() { 88 | return code; 89 | } 90 | 91 | public String getDescription() { 92 | return description; 93 | } 94 | 95 | public FilamentType getType() { 96 | return type; 97 | } 98 | 99 | public static Optional getFilament(final String code) { 100 | return Optional.ofNullable(MAP.get(code)); 101 | } 102 | 103 | public static String getFilamentDescription(final String code, final boolean fullName) { 104 | final Function mapper = fullName ? MAPPER_DESCRIPTION : MAPPER_TYPE; 105 | return Optional.ofNullable(MAP.get(code)) 106 | .map(mapper) 107 | .orElseGet(() -> mapper.apply(UNKNOWN)); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/printer/FilamentType.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.printer; 2 | 3 | import java.util.EnumSet; 4 | import java.util.Map; 5 | import java.util.Optional; 6 | import java.util.function.Function; 7 | import java.util.stream.Collectors; 8 | 9 | /** 10 | * 11 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 12 | */ 13 | public enum FilamentType { 14 | UNKNOWN("Unknown"), 15 | ABS("ABS"), 16 | ASA("ASA"), 17 | HIPS("HIPS"), 18 | PA("PA"), 19 | PVA("PVA"), 20 | PACF("PA-CF"), 21 | PA6CF("PA6-CF"), 22 | PC("PC"), 23 | PETCF("PET-CF"), 24 | PETG("PETG"), 25 | PETGCF("PETG-CF"), 26 | PLA("PLA"), 27 | PLA_AERO("PLA-AERO"), 28 | PLA_CF("PLA-CF"), 29 | TPU("TPU"); 30 | 31 | private static final Map MAP = EnumSet.allOf(FilamentType.class).stream().collect(Collectors.toMap(FilamentType::getDescription, Function.identity())); 32 | 33 | private final String description; 34 | 35 | private FilamentType(final String description) { 36 | this.description = description; 37 | } 38 | 39 | public String getDescription() { 40 | return description; 41 | } 42 | 43 | public static Optional getFilamentType(final String code) { 44 | return Optional.ofNullable(MAP.get(code)); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/security/NavigationAccessCheckerInitializer.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.security; 2 | 3 | import com.tfyre.bambu.view.LoginView; 4 | import com.vaadin.flow.server.ServiceInitEvent; 5 | import com.vaadin.flow.server.VaadinServiceInitListener; 6 | import com.vaadin.flow.server.auth.NavigationAccessControl; 7 | 8 | /** 9 | * 10 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 11 | */ 12 | public class NavigationAccessCheckerInitializer implements VaadinServiceInitListener { 13 | 14 | private final NavigationAccessControl accessControl; 15 | 16 | public NavigationAccessCheckerInitializer() { 17 | accessControl = new NavigationAccessControl(); 18 | accessControl.setLoginView(LoginView.class); 19 | } 20 | 21 | @Override 22 | public void serviceInit(final ServiceInitEvent serviceInitEvent) { 23 | serviceInitEvent.getSource().addUIInitListener(uiInitEvent -> { 24 | uiInitEvent.getUI().addBeforeEnterListener(accessControl); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/security/SecurityUtils.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.security; 2 | 3 | import com.tfyre.bambu.SystemRoles; 4 | import com.vaadin.flow.component.notification.Notification; 5 | import com.vaadin.flow.server.VaadinServletRequest; 6 | import io.quarkus.logging.Log; 7 | import java.security.Principal; 8 | import java.util.Optional; 9 | import jakarta.servlet.ServletException; 10 | import jakarta.servlet.http.HttpSession; 11 | 12 | /** 13 | * 14 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 15 | */ 16 | public class SecurityUtils { 17 | 18 | 19 | public static Optional getPrincipal() { 20 | return Optional.ofNullable(VaadinServletRequest.getCurrent()).map(VaadinServletRequest::getUserPrincipal); 21 | } 22 | 23 | public static boolean isLoggedIn() { 24 | return getPrincipal().isPresent(); 25 | } 26 | 27 | public static boolean userHasAccess(final String roleName) { 28 | return Optional.ofNullable(VaadinServletRequest.getCurrent()) 29 | .map(vsr -> vsr.isUserInRole(SystemRoles.ROLE_ADMIN) || vsr.isUserInRole(roleName)) 30 | .orElse(false); 31 | } 32 | 33 | public static boolean userHasRole(final String roleName) { 34 | return Optional.ofNullable(VaadinServletRequest.getCurrent()) 35 | .map(vsr -> vsr.isUserInRole(roleName)) 36 | .orElse(false); 37 | } 38 | 39 | public static boolean login(final String username, final String password) { 40 | return Optional.ofNullable(VaadinServletRequest.getCurrent()) 41 | .map(vsr -> Optional.ofNullable(vsr.getUserPrincipal()).map(p -> { 42 | Notification.show(String.format("Already logged in: %s", p.getName())); 43 | return true; 44 | }).orElseGet(() -> { 45 | try { 46 | vsr.login(username, password); 47 | return true; 48 | } catch (ServletException ex) { 49 | Log.error(String.format("Login failed for [%s] - %s", username, ex.getMessage())); 50 | return false; 51 | } 52 | })).orElseGet(() -> { 53 | Notification.show("VaadinServletRequest is null"); 54 | return false; 55 | }); 56 | } 57 | 58 | public static void logout() { 59 | Optional.ofNullable(VaadinServletRequest.getCurrent()).map(VaadinServletRequest::getSession) 60 | .ifPresent(HttpSession::invalidate); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/FilamentView.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view; 2 | 3 | import com.tfyre.bambu.YesNoCancelDialog; 4 | import com.tfyre.bambu.model.Tray; 5 | import com.tfyre.bambu.printer.BambuConst; 6 | import com.tfyre.bambu.printer.BambuPrinter; 7 | import com.tfyre.bambu.printer.Filament; 8 | import com.vaadin.flow.component.AttachEvent; 9 | import com.vaadin.flow.component.combobox.ComboBox; 10 | import com.vaadin.flow.component.customfield.CustomField; 11 | import com.vaadin.flow.component.formlayout.FormLayout; 12 | import com.vaadin.flow.component.html.Div; 13 | import com.vaadin.flow.component.html.Input; 14 | import com.vaadin.flow.component.textfield.IntegerField; 15 | import io.quarkus.logging.Log; 16 | import jakarta.xml.bind.DatatypeConverter; 17 | import java.util.Comparator; 18 | import java.util.EnumSet; 19 | import java.util.List; 20 | import java.util.Optional; 21 | 22 | /** 23 | * 24 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 25 | */ 26 | public class FilamentView extends FormLayout implements NotificationHelper, ViewHelper { 27 | 28 | private final ComboBox filaments = new ComboBox<>("Filament"); 29 | private final ColorField color = new ColorField("Custom Color"); 30 | private final IntegerField minTemp = new IntegerField("Min Temperature"); 31 | private final IntegerField maxTemp = new IntegerField("Max Temperature"); 32 | 33 | private static Optional fromPrinter(final BambuPrinter printer, final int amsId, final int trayId) { 34 | final String ams = Integer.toString(amsId); 35 | final String tray = Integer.toString(trayId); 36 | return printer.getFullStatus() 37 | .filter(m -> m.message().hasPrint()) 38 | .map(m -> m.message().getPrint()) 39 | .filter(p -> p.hasAms()) 40 | .map(p -> p.getAms()) 41 | .flatMap(a -> a.getAmsList().stream().filter(s -> ams.equals(s.getId())) 42 | .findAny() 43 | ) 44 | .flatMap(trays -> trays.getTrayList().stream().filter(t -> tray.equals(t.getId())) 45 | .findAny() 46 | ); 47 | } 48 | 49 | public static void show(final BambuPrinter printer, final int amsId, final int trayId) { 50 | final FilamentView view = new FilamentView().build(printer.getName(), fromPrinter(printer, amsId, trayId)); 51 | 52 | YesNoCancelDialog.show(List.of(view), "Change filament?", ync -> { 53 | if (!ync.isConfirmed()) { 54 | return; 55 | } 56 | final Filament filament = view.filaments.getValue(); 57 | if (filament == null || filament == Filament.UNKNOWN) { 58 | view.showError("Please select a valid filament"); 59 | return; 60 | } 61 | final Optional color = view.getColor(); 62 | if (color.isEmpty()) { 63 | view.showError("Please select a valid color"); 64 | return; 65 | } 66 | 67 | printer.commandFilamentSetting(amsId, trayId, filament, color.get(), 68 | view.minTemp.getValue(), view.maxTemp.getValue()); 69 | printer.commandFullStatus(true); 70 | }); 71 | 72 | } 73 | 74 | private Optional getColor() { 75 | final String c = color.getValue(); 76 | if (c == null || c.length() != 7) { 77 | return Optional.empty(); 78 | } 79 | if (!c.startsWith("#")) { 80 | return Optional.empty(); 81 | } 82 | 83 | final String result = c.substring(1); 84 | try { 85 | DatatypeConverter.parseHexBinary(result); 86 | } catch (IllegalArgumentException ex) { 87 | Log.errorf("[%s] is not a valid color: %s", result, ex.getMessage()); 88 | return Optional.empty(); 89 | } 90 | 91 | return Optional.of("%sFF".formatted(result.toUpperCase())); 92 | } 93 | 94 | private List getFilaments() { 95 | return EnumSet.allOf(Filament.class).stream() 96 | .sorted(Comparator.comparing(Filament::getDescription, String.CASE_INSENSITIVE_ORDER)) 97 | .toList(); 98 | } 99 | 100 | private void setTemp(final IntegerField field) { 101 | field.setMin(0); 102 | field.setMax(BambuConst.TEMPERATURE_MAX_NOZZLE); 103 | field.setStepButtonsVisible(true); 104 | } 105 | 106 | public FilamentView build(final String printerName, final Optional tray) { 107 | addClassName("filament-view"); 108 | filaments.setItemLabelGenerator(Filament::getDescription); 109 | filaments.setItems(getFilaments()); 110 | 111 | setTemp(minTemp); 112 | setTemp(maxTemp); 113 | 114 | minTemp.setValue(190); 115 | maxTemp.setValue(220); 116 | 117 | tray.ifPresent(t -> { 118 | filaments.setValue(Filament.getFilament(t.getTrayInfoIdx()).orElse(Filament.UNKNOWN)); 119 | color.setValue("#%.6s".formatted(t.getTrayColor())); 120 | minTemp.setValue(parseInt(printerName, t.getNozzleTempMin(), 190)); 121 | maxTemp.setValue(parseInt(printerName, t.getNozzleTempMax(), 220)); 122 | }); 123 | 124 | add(filaments, color, minTemp, maxTemp); 125 | setColspan(color, 2); 126 | 127 | return this; 128 | } 129 | 130 | public final class ColorField extends CustomField { 131 | 132 | private final Input input = new Input(); 133 | 134 | public ColorField(final String label) { 135 | input.setType("color"); 136 | setLabel(label); 137 | } 138 | 139 | @Override 140 | public final void setLabel(final String label) { 141 | super.setLabel(label); 142 | } 143 | 144 | @Override 145 | protected void onAttach(final AttachEvent attachEvent) { 146 | super.onAttach(attachEvent); 147 | 148 | final Div presets = new Div(); 149 | add(input, presets); 150 | presets.addClassName("presets"); 151 | 152 | EnumSet.allOf(BambuConst.Color.class).forEach(c -> { 153 | final Div preset = new Div(); 154 | preset.addClassName("preset"); 155 | preset.getStyle().setBackgroundColor(c.getHtmlColor()); 156 | preset.addClickListener(l -> { 157 | input.setValue(c.getHtmlColor()); 158 | updateValue(); 159 | }); 160 | presets.add(preset); 161 | }); 162 | } 163 | 164 | @Override 165 | protected String generateModelValue() { 166 | return input.getValue(); 167 | } 168 | 169 | @Override 170 | protected void setPresentationValue(final String color) { 171 | input.setValue(color); 172 | } 173 | 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/GCodeDialog.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view; 2 | 3 | import com.tfyre.bambu.printer.BambuPrinter; 4 | import com.vaadin.flow.component.Unit; 5 | import com.vaadin.flow.component.button.Button; 6 | import com.vaadin.flow.component.button.ButtonVariant; 7 | import com.vaadin.flow.component.dialog.Dialog; 8 | import com.vaadin.flow.component.textfield.TextArea; 9 | 10 | /** 11 | * 12 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 13 | */ 14 | public class GCodeDialog { 15 | 16 | public static void show(final BambuPrinter printer) { 17 | final Dialog d = new Dialog(); 18 | d.setHeaderTitle("Send GCode (No Validation!!)"); 19 | final TextArea text = new TextArea(); 20 | text.setWidthFull(); 21 | text.setHeight(95, Unit.PERCENTAGE); 22 | d.add(text); 23 | final Button cancel = new Button("Cancel", e -> d.close()); 24 | cancel.addThemeVariants(ButtonVariant.LUMO_ERROR); 25 | final Button ok = new Button("OK", e -> { 26 | d.close(); 27 | printer.commandPrintGCodeLine(text.getValue().trim().replaceAll("\n", "\\\n")); 28 | }); 29 | ok.addThemeVariants(ButtonVariant.LUMO_PRIMARY); 30 | d.getFooter().add(cancel, ok); 31 | d.setWidth(80, Unit.PERCENTAGE); 32 | d.setHeight(80, Unit.PERCENTAGE); 33 | d.open(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/GridHelper.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view; 2 | 3 | import com.vaadin.flow.component.checkbox.Checkbox; 4 | import com.vaadin.flow.component.grid.Grid; 5 | import com.vaadin.flow.data.renderer.ComponentRenderer; 6 | import com.vaadin.flow.function.ValueProvider; 7 | 8 | /** 9 | * 10 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 11 | */ 12 | public interface GridHelper { 13 | 14 | Grid getGrid(); 15 | 16 | default ComponentRenderer getCheckboxRenderer(final ValueProvider valueProvider) { 17 | return new ComponentRenderer<>((source) -> { 18 | final Checkbox result = new Checkbox(); 19 | result.setReadOnly(true); 20 | result.setValue(valueProvider.apply(source)); 21 | return result; 22 | }); 23 | } 24 | 25 | default Grid.Column setupColumn(final String name, final ValueProvider valueProvider) { 26 | return getGrid().addColumn(valueProvider) 27 | .setHeader(name); 28 | } 29 | 30 | default Grid.Column setupColumn(final String name, final ComponentRenderer component) { 31 | return getGrid().addColumn(component) 32 | .setHeader(name); 33 | } 34 | 35 | default Grid.Column setupColumnCheckBox(final String name, final ValueProvider valueProvider) { 36 | return getGrid().addColumn(getCheckboxRenderer(valueProvider)) 37 | .setHeader(name); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/LoginView.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view; 2 | 3 | import com.tfyre.bambu.BambuConfig; 4 | import com.tfyre.bambu.MainLayout; 5 | import com.tfyre.bambu.SystemRoles; 6 | import com.tfyre.bambu.security.SecurityUtils; 7 | import com.vaadin.flow.component.AttachEvent; 8 | import com.vaadin.flow.component.UI; 9 | import com.vaadin.flow.component.html.H1; 10 | import com.vaadin.flow.component.login.AbstractLogin; 11 | import com.vaadin.flow.component.login.LoginForm; 12 | import com.vaadin.flow.component.orderedlayout.FlexComponent; 13 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 14 | import com.vaadin.flow.router.BeforeEnterEvent; 15 | import com.vaadin.flow.router.BeforeEnterObserver; 16 | import com.vaadin.flow.router.PageTitle; 17 | import com.vaadin.flow.router.Route; 18 | import com.vaadin.flow.server.VaadinSession; 19 | import static com.vaadin.flow.server.auth.NavigationAccessControl.SESSION_STORED_REDIRECT; 20 | import jakarta.inject.Inject; 21 | import java.util.Optional; 22 | 23 | /** 24 | * 25 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 26 | */ 27 | @Route(LoginView.LOGIN) 28 | @PageTitle("Login") 29 | public class LoginView extends VerticalLayout implements BeforeEnterObserver, NotificationHelper { 30 | 31 | protected static final String LOGIN = "login"; 32 | 33 | public static final String LOGIN_SUCCESS_URL = "/"; 34 | 35 | private final LoginForm login = new LoginForm(); 36 | 37 | @Inject 38 | BambuConfig config; 39 | 40 | @Override 41 | protected void onAttach(final AttachEvent attachEvent) { 42 | super.onAttach(attachEvent); 43 | addClassName("login-view"); 44 | setSizeFull(); 45 | 46 | setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); 47 | setAlignItems(FlexComponent.Alignment.CENTER); 48 | 49 | login.addLoginListener(e -> doLogin(e.getUsername(), e.getPassword())); 50 | login.addForgotPasswordListener(this::onForgotPassword); 51 | 52 | add(new H1("Bambu Web Farm"), login); 53 | if (config.darkMode()) { 54 | MainLayout.setTheme(getElement(), true); 55 | } 56 | } 57 | 58 | @Override 59 | public void beforeEnter(BeforeEnterEvent beforeEnterEvent) { 60 | if (beforeEnterEvent.getLocation().getQueryParameters().getParameters().containsKey("error")) { 61 | login.setError(true); 62 | } else if (SecurityUtils.isLoggedIn()) { 63 | UI.getCurrent().getPage().setLocation(getLoggedInUrl()); 64 | } else { 65 | if (config.autoLogin()) { 66 | doLogin(SystemRoles.ROLE_ADMIN, SystemRoles.ROLE_ADMIN); 67 | } 68 | } 69 | } 70 | 71 | private void doLogin(final String username, final String password) { 72 | final boolean authenticated = SecurityUtils.login(username, password); 73 | if (authenticated) { 74 | UI.getCurrent().getPage().setLocation(getLoggedInUrl()); 75 | } else { 76 | login.setError(true); 77 | } 78 | } 79 | 80 | private String getLoggedInUrl() { 81 | return Optional.ofNullable(VaadinSession.getCurrent()) 82 | .map(vs -> String.class.cast(vs.getSession().getAttribute(SESSION_STORED_REDIRECT))) 83 | .map(s -> s.isBlank() || s.startsWith(LOGIN) ? LOGIN_SUCCESS_URL : s) 84 | .orElse(LOGIN_SUCCESS_URL); 85 | } 86 | 87 | private void onForgotPassword(AbstractLogin.ForgotPasswordEvent event) { 88 | showError("This has not been implemented"); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/LogsView.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.google.protobuf.InvalidProtocolBufferException; 6 | import com.google.protobuf.util.JsonFormat; 7 | import com.tfyre.bambu.MainLayout; 8 | import com.tfyre.bambu.SystemRoles; 9 | import com.tfyre.bambu.model.BambuMessage; 10 | import com.tfyre.bambu.printer.BambuPrinter; 11 | import com.tfyre.bambu.printer.BambuPrinters; 12 | import com.vaadin.flow.component.AttachEvent; 13 | import com.vaadin.flow.component.Component; 14 | import com.vaadin.flow.component.HasComponents; 15 | import com.vaadin.flow.component.Unit; 16 | import com.vaadin.flow.component.button.Button; 17 | import com.vaadin.flow.component.combobox.ComboBox; 18 | import com.vaadin.flow.component.html.Span; 19 | import com.vaadin.flow.component.icon.Icon; 20 | import com.vaadin.flow.component.icon.VaadinIcon; 21 | import com.vaadin.flow.component.listbox.ListBox; 22 | import com.vaadin.flow.component.orderedlayout.FlexLayout; 23 | import com.vaadin.flow.component.orderedlayout.HorizontalLayout; 24 | import com.vaadin.flow.component.orderedlayout.Scroller; 25 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 26 | import com.vaadin.flow.component.textfield.TextArea; 27 | import com.vaadin.flow.component.textfield.TextField; 28 | import com.vaadin.flow.data.value.ValueChangeMode; 29 | import com.vaadin.flow.router.BeforeEvent; 30 | import com.vaadin.flow.router.HasUrlParameter; 31 | import com.vaadin.flow.router.OptionalParameter; 32 | import com.vaadin.flow.router.PageTitle; 33 | import com.vaadin.flow.router.Route; 34 | import jakarta.annotation.security.RolesAllowed; 35 | import jakarta.inject.Inject; 36 | import java.time.format.DateTimeFormatter; 37 | import java.time.format.DateTimeFormatterBuilder; 38 | import java.time.temporal.ChronoField; 39 | import java.util.ArrayList; 40 | import java.util.Comparator; 41 | import java.util.List; 42 | import java.util.Optional; 43 | 44 | /** 45 | * 46 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 47 | */ 48 | @Route(value = "logs", layout = MainLayout.class) 49 | @PageTitle("Logs") 50 | @RolesAllowed({ SystemRoles.ROLE_ADMIN }) 51 | public class LogsView extends VerticalLayout implements HasUrlParameter, NotificationHelper, UpdateHeader { 52 | 53 | private static final ObjectMapper OM = new ObjectMapper(); 54 | private static final JsonFormat.Printer PRINTER = JsonFormat.printer().preservingProtoFieldNames(); 55 | //DateTimeFormatter.ISO_DATE_TIME; 56 | private static final DateTimeFormatter DTF = new DateTimeFormatterBuilder() 57 | .parseCaseInsensitive() 58 | .append(DateTimeFormatter.ISO_LOCAL_DATE) 59 | .appendLiteral(' ') 60 | .appendValue(ChronoField.HOUR_OF_DAY, 2) 61 | .appendLiteral(':') 62 | .appendValue(ChronoField.MINUTE_OF_HOUR, 2) 63 | .optionalStart() 64 | .appendLiteral(':') 65 | .appendValue(ChronoField.SECOND_OF_MINUTE, 2) 66 | .appendLiteral(".") 67 | .appendValue(ChronoField.MILLI_OF_SECOND, 3) 68 | .toFormatter(); 69 | 70 | @Inject 71 | BambuPrinters printers; 72 | 73 | private Optional _printer = Optional.empty(); 74 | private final ListBox listBox = new ListBox<>(); 75 | private final TextField filter = new TextField(); 76 | private final TextArea json = new TextArea("RAW"); 77 | private final TextArea parsed = new TextArea("Parsed"); 78 | private final List messages = new ArrayList<>(); 79 | private final ComboBox comboBox = new ComboBox<>(); 80 | 81 | @Override 82 | public void setParameter(final BeforeEvent event, @OptionalParameter final String printerName) { 83 | _printer = printers.getPrinter(printerName); 84 | } 85 | 86 | private String parseJson(final String data) { 87 | try { 88 | return OM.writerWithDefaultPrettyPrinter().writeValueAsString(OM.readTree(data)); 89 | } catch (JsonProcessingException ex) { 90 | showError(ex.getMessage()); 91 | return data; 92 | } 93 | } 94 | 95 | private String parseMessage(final BambuMessage message) { 96 | try { 97 | return PRINTER.print(message); 98 | } catch (InvalidProtocolBufferException ex) { 99 | showError(ex.getMessage()); 100 | return message.toString(); 101 | } 102 | } 103 | 104 | private void buildFilter() { 105 | final String value = filter.getValue(); 106 | if (value == null || value.isBlank()) { 107 | listBox.setItems(messages); 108 | return; 109 | } 110 | 111 | listBox.setItems(messages.stream().filter(m -> m.raw().contains(value)).toList()); 112 | } 113 | 114 | private void buildList(final BambuPrinter printer) { 115 | messages.clear(); 116 | messages.addAll(new ArrayList<>(printer.getLastMessages()).reversed()); 117 | buildFilter(); 118 | } 119 | 120 | private Component buildListBox() { 121 | listBox.setItemLabelGenerator(m -> "%s - %s".formatted(DTF.format(m.lastUpdated()), m.raw().length())); 122 | listBox.addValueChangeListener(l -> { 123 | if (l.getValue() == null) { 124 | return; 125 | } 126 | json.setValue(parseJson(l.getValue().raw())); 127 | parsed.setValue(parseMessage(l.getValue().message())); 128 | }); 129 | listBox.setMinWidth(300, Unit.PIXELS); 130 | 131 | json.setReadOnly(true); 132 | parsed.setReadOnly(true); 133 | 134 | final FlexLayout flex = new FlexLayout(json, parsed); 135 | flex.setFlexGrow(50.0, json, parsed); 136 | final HorizontalLayout result = new HorizontalLayout(); 137 | result.setSizeFull(); 138 | result.add(listBox); 139 | result.addAndExpand(new Scroller(flex)); 140 | result.setMinHeight("0"); 141 | return result; 142 | } 143 | 144 | private void buildToolbar() { 145 | comboBox.setItemLabelGenerator(BambuPrinter::getName); 146 | comboBox.setItems(printers.getPrinters().stream().sorted(Comparator.comparing(BambuPrinter::getName)).toList()); 147 | comboBox.addValueChangeListener(l -> buildList(l.getValue())); 148 | filter.addValueChangeListener(l -> buildFilter()); 149 | filter.setValueChangeMode(ValueChangeMode.TIMEOUT); 150 | } 151 | 152 | @Override 153 | protected void onAttach(final AttachEvent attachEvent) { 154 | super.onAttach(attachEvent); 155 | addClassName("logs-view"); 156 | setSizeFull(); 157 | buildToolbar(); 158 | add(buildListBox()); 159 | _printer.ifPresent(comboBox::setValue); 160 | } 161 | 162 | @Override 163 | public void updateHeader(final HasComponents component) { 164 | final Button refresh = new Button("Refresh", new Icon(VaadinIcon.REFRESH), l -> Optional.ofNullable(comboBox.getValue()).ifPresent(this::buildList)); 165 | component.add(new Span("Printers"), comboBox, refresh, new Span("Filter"), filter); 166 | } 167 | 168 | //FIXME Implement Export 169 | private record Entry(String date, String raw, String parsed) { 170 | 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/MaintenanceView.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view; 2 | 3 | import com.tfyre.bambu.MainLayout; 4 | import com.tfyre.bambu.SystemRoles; 5 | import com.tfyre.bambu.printer.BambuPrinter; 6 | import com.tfyre.bambu.printer.BambuPrinterConsumer; 7 | import com.tfyre.bambu.printer.BambuPrinterException; 8 | import com.tfyre.bambu.printer.BambuPrinters; 9 | import com.vaadin.flow.component.AttachEvent; 10 | import com.vaadin.flow.component.HasComponents; 11 | import com.vaadin.flow.component.UI; 12 | import com.vaadin.flow.component.button.Button; 13 | import com.vaadin.flow.component.grid.Grid; 14 | import com.vaadin.flow.component.grid.GridSortOrder; 15 | import com.vaadin.flow.component.icon.Icon; 16 | import com.vaadin.flow.component.icon.VaadinIcon; 17 | import com.vaadin.flow.component.orderedlayout.HorizontalLayout; 18 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 19 | import com.vaadin.flow.router.PageTitle; 20 | import com.vaadin.flow.router.Route; 21 | import io.quarkus.logging.Log; 22 | import jakarta.annotation.security.RolesAllowed; 23 | import jakarta.inject.Inject; 24 | import java.time.OffsetDateTime; 25 | import java.time.format.DateTimeFormatter; 26 | import java.time.format.DateTimeFormatterBuilder; 27 | import java.time.temporal.ChronoField; 28 | import java.util.Comparator; 29 | import java.util.Optional; 30 | import java.util.function.Function; 31 | import org.eclipse.microprofile.context.ManagedExecutor; 32 | 33 | /** 34 | * 35 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 36 | */ 37 | @Route(value = "maintenance", layout = MainLayout.class) 38 | @PageTitle("Maintenance") 39 | @RolesAllowed({ SystemRoles.ROLE_ADMIN }) 40 | public class MaintenanceView extends VerticalLayout implements NotificationHelper, GridHelper, UpdateHeader { 41 | 42 | private static final DateTimeFormatter DTF = new DateTimeFormatterBuilder() 43 | .parseCaseInsensitive() 44 | .append(DateTimeFormatter.ISO_LOCAL_DATE) 45 | .appendLiteral(' ') 46 | .appendValue(ChronoField.HOUR_OF_DAY, 2) 47 | .appendLiteral(':') 48 | .appendValue(ChronoField.MINUTE_OF_HOUR, 2) 49 | .optionalStart() 50 | .appendLiteral(':') 51 | .appendValue(ChronoField.SECOND_OF_MINUTE, 2) 52 | .toFormatter(); 53 | 54 | @Inject 55 | BambuPrinters printers; 56 | 57 | @Inject 58 | ManagedExecutor executor; 59 | 60 | private final Grid grid = new Grid<>(); 61 | 62 | @Override 63 | public Grid getGrid() { 64 | return grid; 65 | } 66 | 67 | @Override 68 | protected void onAttach(final AttachEvent attachEvent) { 69 | super.onAttach(attachEvent); 70 | addClassName("maintenance-view"); 71 | setSizeFull(); 72 | configureGrid(); 73 | add(grid); 74 | refreshItems(); 75 | } 76 | 77 | private Button newButton(final BambuPrinters.PrinterDetail pd, final String action, final VaadinIcon icon, final BambuPrinterConsumer consumer) { 78 | final Button result = new Button("", new Icon(icon), l -> { 79 | final Optional ui = getUI(); 80 | executor.submit(() -> { 81 | try { 82 | consumer.accept(pd.name()); 83 | } catch (BambuPrinterException ex) { 84 | Log.error(ex.getMessage(), ex); 85 | ui.get().access(() -> { 86 | showError(ex.getMessage()); 87 | refreshItems(); 88 | }); 89 | } 90 | }); 91 | }); 92 | result.setTooltipText(action); 93 | return result; 94 | } 95 | 96 | private void refreshItems() { 97 | grid.setItems(printers.getPrintersDetail()); 98 | } 99 | 100 | private Comparator getODTComparator( 101 | final Function> function1, 102 | final Function function2) { 103 | return Comparator.comparing(pd -> 104 | function1.apply(pd.printer()) 105 | .map(function2) 106 | .map(odt -> odt.toEpochSecond()) 107 | .orElse(0l) 108 | ); 109 | } 110 | 111 | private void configureGrid() { 112 | final Grid.Column colName 113 | = setupColumn("Name", pd -> pd.printer().getName()); 114 | setupColumnCheckBox("Running", pd -> pd.isRunning()); 115 | setupColumn("Last Status", pd -> pd.printer().getStatus().map(m -> DTF.format(m.lastUpdated())).orElse("--")) 116 | .setSortable(true).setComparator(getODTComparator(BambuPrinter::getStatus, BambuPrinter.Message::lastUpdated)); 117 | setupColumn("Last Full Status", pd -> pd.printer().getFullStatus().map(m -> DTF.format(m.lastUpdated())).orElse("--")) 118 | .setSortable(true).setComparator(getODTComparator(BambuPrinter::getFullStatus, BambuPrinter.Message::lastUpdated)); 119 | setupColumn("Last Thumbnail", pd -> pd.printer().getThumbnail().map(m -> DTF.format(m.lastUpdated())).orElse("--")) 120 | .setSortable(true).setComparator(getODTComparator(BambuPrinter::getThumbnail, BambuPrinter.Thumbnail::lastUpdated)); 121 | 122 | grid.addComponentColumn(v -> { 123 | final Button gcode = new Button("", new Icon(VaadinIcon.COG), l -> GCodeDialog.show(v.printer())); 124 | gcode.setTooltipText("Send GCode"); 125 | return new HorizontalLayout( 126 | newButton(v, "Enable", VaadinIcon.PLAY, printers::startPrinter), 127 | newButton(v, "Disable", VaadinIcon.STOP, printers::stopPrinter), 128 | gcode 129 | ); 130 | }); 131 | 132 | grid.sort(GridSortOrder.asc(colName).build()); 133 | } 134 | 135 | @Override 136 | public void updateHeader(final HasComponents component) { 137 | component.add(new Button("Refresh", new Icon(VaadinIcon.REFRESH), l -> refreshItems())); 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/NotificationHelper.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view; 2 | 3 | import com.vaadin.flow.component.HasText; 4 | import com.vaadin.flow.component.html.Span; 5 | import com.vaadin.flow.component.notification.Notification; 6 | import com.vaadin.flow.component.notification.NotificationVariant; 7 | import io.quarkus.logging.Log; 8 | import java.time.Duration; 9 | import org.jboss.logging.Logger.Level; 10 | 11 | /** 12 | * 13 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 14 | */ 15 | public interface NotificationHelper { 16 | 17 | default Notification getNotificationSpan(final String content) { 18 | final Span s = new Span(content); 19 | if (content.contains("\n")) { 20 | s.setWhiteSpace(HasText.WhiteSpace.PRE); 21 | } 22 | final Notification n = new Notification(s); 23 | s.addClickListener(e -> n.close()); 24 | return n; 25 | } 26 | 27 | default void showError(final String error, final int duration) { 28 | final Notification n = getNotificationSpan(error); 29 | n.setDuration(duration); 30 | n.addThemeVariants(NotificationVariant.LUMO_ERROR); 31 | n.setPosition(Notification.Position.BOTTOM_END); 32 | n.open(); 33 | } 34 | 35 | default void showError(final String error) { 36 | showError(error, -1); 37 | } 38 | 39 | default void showError(final String error, final Duration duration) { 40 | showError(error, (int) duration.toMillis()); 41 | } 42 | 43 | default void showError(final Throwable ex) { 44 | showError(ex.getMessage()); 45 | } 46 | 47 | default void showNotification(final String message, final int duration) { 48 | final Notification n = getNotificationSpan(message); 49 | n.setDuration(duration); 50 | n.addThemeVariants(NotificationVariant.LUMO_SUCCESS); 51 | n.setPosition(Notification.Position.BOTTOM_END); 52 | n.open(); 53 | } 54 | 55 | default void showNotification(final String message) { 56 | showNotification(message, Duration.ofSeconds(3)); 57 | } 58 | 59 | default void showNotification(final String message, final Duration duration) { 60 | showNotification(message, (int) duration.toMillis()); 61 | } 62 | 63 | default void showErrorLog(final Throwable ex) { 64 | showError(ex); 65 | Log.error(ex.getMessage(), ex); 66 | } 67 | 68 | default void showWarning(final String warning) { 69 | final Notification n = getNotificationSpan(warning); 70 | n.setDuration(1500); 71 | n.addThemeVariants(NotificationVariant.LUMO_CONTRAST); 72 | n.setPosition(Notification.Position.BOTTOM_END); 73 | n.open(); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/PrinterView.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view; 2 | 3 | import com.tfyre.bambu.BambuConfig; 4 | import com.tfyre.bambu.MainLayout; 5 | import com.tfyre.bambu.SystemRoles; 6 | import com.tfyre.bambu.printer.BambuPrinter; 7 | import com.tfyre.bambu.printer.BambuPrinters; 8 | import com.tfyre.bambu.view.dashboard.DashboardPrinter; 9 | import com.vaadin.flow.component.AttachEvent; 10 | import com.vaadin.flow.component.Component; 11 | import com.vaadin.flow.component.HasComponents; 12 | import com.vaadin.flow.component.UI; 13 | import com.vaadin.flow.component.combobox.ComboBox; 14 | import com.vaadin.flow.component.html.Div; 15 | import com.vaadin.flow.component.html.Span; 16 | import com.vaadin.flow.router.BeforeEvent; 17 | import com.vaadin.flow.router.HasUrlParameter; 18 | import com.vaadin.flow.router.OptionalParameter; 19 | import com.vaadin.flow.router.PageTitle; 20 | import com.vaadin.flow.router.Route; 21 | import jakarta.annotation.security.RolesAllowed; 22 | import jakarta.enterprise.inject.Instance; 23 | import jakarta.inject.Inject; 24 | import java.util.Comparator; 25 | import java.util.Optional; 26 | 27 | /** 28 | * 29 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 30 | */ 31 | @Route(value = "printer", layout = MainLayout.class) 32 | @PageTitle("Printer") 33 | @RolesAllowed({ SystemRoles.ROLE_ADMIN }) 34 | public class PrinterView extends PushDiv implements HasUrlParameter, NotificationHelper, UpdateHeader { 35 | 36 | @Inject 37 | BambuPrinters printers; 38 | @Inject 39 | Instance cardInstance; 40 | @Inject 41 | BambuConfig config; 42 | 43 | private Optional _printer = Optional.empty(); 44 | private final ComboBox comboBox = new ComboBox<>(); 45 | private final Div content = new Div(); 46 | 47 | @Override 48 | public void setParameter(final BeforeEvent event, @OptionalParameter final String printerName) { 49 | _printer = printers.getPrinter(printerName); 50 | } 51 | 52 | private void buildPrinter(final BambuPrinter printer) { 53 | content.removeAll(); 54 | final DashboardPrinter card = cardInstance.get(); 55 | content.add(card.build(printer, false)); 56 | final UI ui = getUI().get(); 57 | createFuture(() -> ui.access(card::update), config.refreshInterval()); 58 | } 59 | 60 | private Component buildContent() { 61 | content.setClassName("content"); 62 | return content; 63 | } 64 | 65 | @Override 66 | protected void onAttach(final AttachEvent attachEvent) { 67 | super.onAttach(attachEvent); 68 | addClassName("printer-view"); 69 | add(buildContent()); 70 | comboBox.setItemLabelGenerator(BambuPrinter::getName); 71 | comboBox.setItems(printers.getPrinters().stream().sorted(Comparator.comparing(BambuPrinter::getName)).toList()); 72 | comboBox.addValueChangeListener(l -> buildPrinter(l.getValue())); 73 | _printer.ifPresent(comboBox::setValue); 74 | } 75 | 76 | @Override 77 | public void updateHeader(final HasComponents component) { 78 | component.add(new Span("Printers"), comboBox); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/PushDiv.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view; 2 | 3 | import com.vaadin.flow.component.DetachEvent; 4 | import com.vaadin.flow.component.html.Div; 5 | import com.vaadin.flow.component.orderedlayout.FlexComponent; 6 | import jakarta.inject.Inject; 7 | import java.time.Duration; 8 | import java.util.Optional; 9 | import java.util.concurrent.ScheduledExecutorService; 10 | import java.util.concurrent.ScheduledFuture; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | /** 14 | * 15 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 16 | */ 17 | public abstract class PushDiv extends Div implements FlexComponent { 18 | 19 | @Inject 20 | ScheduledExecutorService ses; 21 | 22 | private Optional> future = Optional.empty(); 23 | 24 | private void cancelFuture() { 25 | future.ifPresent(f -> f.cancel(true)); 26 | future = Optional.empty(); 27 | } 28 | 29 | @Override 30 | protected void onDetach(final DetachEvent detachEvent) { 31 | super.onDetach(detachEvent); 32 | cancelFuture(); 33 | } 34 | 35 | public ScheduledFuture createFuture(final Runnable runnable, final Duration interval) { 36 | cancelFuture(); 37 | future = Optional.of(ses.scheduleAtFixedRate(runnable, 0, interval.getSeconds(), TimeUnit.SECONDS)); 38 | return future.get(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/UpdateHeader.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view; 2 | 3 | import com.vaadin.flow.component.HasComponents; 4 | 5 | /** 6 | * 7 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 8 | */ 9 | public interface UpdateHeader { 10 | 11 | void updateHeader(final HasComponents component); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/ViewHelper.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view; 2 | 3 | import com.tfyre.bambu.YesNoCancelDialog; 4 | import com.vaadin.flow.component.Component; 5 | import com.vaadin.flow.component.html.Div; 6 | import com.vaadin.flow.component.html.Span; 7 | import com.vaadin.flow.component.progressbar.ProgressBar; 8 | import io.quarkus.logging.Log; 9 | import java.time.Duration; 10 | 11 | /** 12 | * 13 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 14 | */ 15 | public interface ViewHelper { 16 | 17 | default double parseDouble(final String printerName, final String value, final double defaultValue) { 18 | if (value == null || value.isBlank()) { 19 | return 0; 20 | } 21 | try { 22 | return Double.parseDouble(value); 23 | } catch (NumberFormatException ex) { 24 | Log.errorf("%s: Cannot parseDouble [%s]", printerName, value); 25 | return defaultValue; 26 | } 27 | } 28 | 29 | default double parseDouble(final String value, final double defaultValue) { 30 | if (value == null || value.isBlank()) { 31 | return 0; 32 | } 33 | try { 34 | return Double.parseDouble(value); 35 | } catch (NumberFormatException ex) { 36 | Log.errorf("Cannot parseDouble [%s]", value); 37 | return defaultValue; 38 | } 39 | } 40 | 41 | default int parseInt(final String printerName, final String value, final int defaultValue) { 42 | if (value == null || value.isBlank()) { 43 | return 0; 44 | } 45 | try { 46 | return Integer.parseInt(value); 47 | } catch (NumberFormatException ex) { 48 | Log.errorf("s: Cannot parseInt [%s]", printerName, value); 49 | return defaultValue; 50 | } 51 | } 52 | 53 | default int parseInt(final String value, final int defaultValue) { 54 | if (value == null || value.isBlank()) { 55 | return 0; 56 | } 57 | try { 58 | return Integer.parseInt(value); 59 | } catch (NumberFormatException ex) { 60 | Log.errorf("Cannot parseInt [%s]", value); 61 | return defaultValue; 62 | } 63 | } 64 | 65 | default String formatTime(final Duration duration) { 66 | final StringBuilder sb = new StringBuilder(); 67 | final long days = duration.toDays(); 68 | if (days > 0) { 69 | sb.append(days) 70 | .append(" day(s) "); 71 | } 72 | sb 73 | .append(duration.toHoursPart()) 74 | .append(" hour(s) ") 75 | .append(duration.toMinutesPart()) 76 | .append(" minute(s)"); 77 | return sb.toString(); 78 | } 79 | 80 | default Div newDiv(final String className, final Component... components) { 81 | final Div result = new Div(components); 82 | result.addClassName(className); 83 | return result; 84 | } 85 | 86 | default Span newSpan(final String className) { 87 | final Span result = new Span(); 88 | result.addClassName(className); 89 | return result; 90 | } 91 | 92 | default ProgressBar newProgressBar() { 93 | final ProgressBar result = new ProgressBar(0.0, 100.0); 94 | result.addClassName("progress"); 95 | return result; 96 | } 97 | 98 | default void doConfirm(final Runnable runnable) { 99 | YesNoCancelDialog.show("Are you sure?", ync -> { 100 | if (!ync.isConfirmed()) { 101 | return; 102 | } 103 | runnable.run(); 104 | }); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/batchprint/FilamentHelper.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view.batchprint; 2 | 3 | import com.tfyre.bambu.view.ViewHelper; 4 | import com.vaadin.flow.component.html.Div; 5 | 6 | /** 7 | * 8 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 9 | */ 10 | public interface FilamentHelper extends ViewHelper { 11 | 12 | default long mapFilamentColor(final String color) { 13 | String _color = color.replace("#", ""); 14 | if (_color.length() > 6) { 15 | _color = _color.substring(0, 6); 16 | } 17 | return Integer.parseInt(_color, 16); 18 | } 19 | 20 | default Div setupFilament(final Div div, final String text, final long color) { 21 | div.setText(text); 22 | if (color < 0) { 23 | return div; 24 | } 25 | div.getStyle().setBackgroundColor("#%06X".formatted(color)); 26 | //https://alienryderflex.com/hsp.html 27 | final long brightness = (((color >> 16 & 0xff) * 299) 28 | + ((color >> 8 & 0xff) * 587) 29 | + ((color & 0xff) * 114)) 30 | / 1000; 31 | if (brightness <= 125) { 32 | div.addClassName("contrast"); 33 | } 34 | return div; 35 | } 36 | 37 | default Div newFilament(final PlateFilament filament) { 38 | return setupFilament(newDiv("filament"), filament.type().getDescription(), filament.color()); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/batchprint/Plate.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view.batchprint; 2 | 3 | import java.time.Duration; 4 | import java.util.List; 5 | 6 | /** 7 | * 8 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 9 | */ 10 | public record Plate(String name, int plateId, Duration prediction, double weight, List filaments) { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/batchprint/PlateFilament.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view.batchprint; 2 | 3 | import com.tfyre.bambu.printer.FilamentType; 4 | 5 | /** 6 | * 7 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 8 | */ 9 | public record PlateFilament(int filamentId, FilamentType type, double weight, long color) { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/batchprint/ProjectException.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view.batchprint; 2 | 3 | /** 4 | * 5 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 6 | */ 7 | public class ProjectException extends Exception { 8 | 9 | public ProjectException(String message) { 10 | super(message); 11 | } 12 | 13 | public ProjectException(final String message, final Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/batchprint/ProjectFile.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view.batchprint; 2 | 3 | import com.tfyre.bambu.printer.FilamentType; 4 | import com.tfyre.schema.Config; 5 | import com.tfyre.schema.Metadata; 6 | import com.vaadin.flow.server.AbstractStreamResource; 7 | import com.vaadin.flow.server.StreamResource; 8 | import io.quarkus.logging.Log; 9 | import jakarta.annotation.PreDestroy; 10 | import jakarta.enterprise.context.Dependent; 11 | import jakarta.xml.bind.JAXBContext; 12 | import jakarta.xml.bind.JAXBException; 13 | import jakarta.xml.bind.Unmarshaller; 14 | import java.io.File; 15 | import java.io.FileInputStream; 16 | import java.io.FileNotFoundException; 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.time.Duration; 20 | import java.util.Comparator; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.Optional; 25 | import java.util.stream.Collectors; 26 | import java.util.zip.ZipEntry; 27 | import java.util.zip.ZipFile; 28 | import javax.xml.transform.stream.StreamSource; 29 | import org.jboss.logging.Logger; 30 | 31 | /** 32 | * 33 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 34 | */ 35 | @Dependent 36 | public class ProjectFile implements FilamentHelper { 37 | 38 | private static final String PLATE_PNG = "Metadata/plate_%d.png"; 39 | private static final String SLICE_INFO = "Metadata/slice_info.config"; 40 | 41 | private final Map thumbnails = new HashMap<>(); 42 | private final JAXBContext context; 43 | private final Unmarshaller unmarshaller; 44 | private ZipFile zipFile; 45 | private List plates; 46 | private String filename; 47 | private File file; 48 | 49 | public ProjectFile() { 50 | try { 51 | context = JAXBContext.newInstance(com.tfyre.schema.ObjectFactory.class); 52 | unmarshaller = context.createUnmarshaller(); 53 | } catch (JAXBException ex) { 54 | throw new RuntimeException("Cannot create JAXB: %s".formatted(ex.getMessage()), ex); 55 | } 56 | } 57 | 58 | private Config getSliceInfo() throws ProjectException { 59 | final ZipEntry sliceEntry = Optional.ofNullable(zipFile.getEntry(SLICE_INFO)) 60 | .orElseThrow(() -> new ProjectException("[%s] not found".formatted(SLICE_INFO))); 61 | try { 62 | return unmarshaller.unmarshal(new StreamSource(zipFile.getInputStream(sliceEntry)), Config.class).getValue(); 63 | } catch (JAXBException | IOException ex) { 64 | throw new ProjectException("Cannot unmarshal [%s]: %s".formatted(SLICE_INFO, ex.getMessage()), ex); 65 | } 66 | } 67 | 68 | private PlateFilament mapFilament(final com.tfyre.schema.Filament filament) { 69 | return new PlateFilament(filament.getId(), 70 | FilamentType.getFilamentType(filament.getType()).orElse(FilamentType.UNKNOWN), 71 | filament.getUsedG(), mapFilamentColor(filament.getColor())); 72 | } 73 | 74 | private Plate mapPlate(final com.tfyre.schema.Plate plate) { 75 | final Map map = plate.getMetadata().stream() 76 | .collect(Collectors.toMap(Metadata::getKey, Metadata::getValueAttr)); 77 | final int plateId = parseInt(map.get("index"), -1); 78 | final int prediction = parseInt(map.get("prediction"), -1); 79 | final double weight = parseDouble(map.get("weight"), -1); 80 | return new Plate("Plate %d".formatted(plateId), plateId, Duration.ofSeconds(prediction), weight, 81 | plate.getFilament().stream().map(this::mapFilament).toList()); 82 | } 83 | 84 | public List getPlates() { 85 | return plates; 86 | } 87 | 88 | public String getFilename() { 89 | return filename; 90 | } 91 | 92 | public long getFileSize() { 93 | return file.length(); 94 | } 95 | 96 | public ProjectFile setup(final String filename, final File file) throws ProjectException { 97 | this.filename = filename; 98 | this.file = file; 99 | try { 100 | zipFile = new ZipFile(file); 101 | } catch (IOException ex) { 102 | throw new ProjectException("Error opening [%s]: %s".formatted(filename, ex.getMessage()), ex); 103 | } 104 | plates = getSliceInfo().getPlate().stream() 105 | .map(this::mapPlate) 106 | .sorted(Comparator.comparing(Plate::plateId)) 107 | .toList(); 108 | return this; 109 | } 110 | 111 | private StreamResource getThumbnail(final int plateId) { 112 | final String platePng = PLATE_PNG.formatted(plateId); 113 | final ZipEntry pngEntry = zipFile.getEntry(platePng); 114 | return new StreamResource("image.jpg", () -> { 115 | try { 116 | return zipFile.getInputStream(pngEntry); 117 | } catch (IOException ex) { 118 | final String message = "Cannot read [%s]: %s".formatted(platePng, ex.getMessage()); 119 | Log.error(message, ex); 120 | throw new RuntimeException(message); 121 | } 122 | }); 123 | } 124 | 125 | public AbstractStreamResource getThumbnail(final Plate plate) { 126 | return thumbnails.computeIfAbsent(plate.plateId(), this::getThumbnail); 127 | 128 | } 129 | 130 | @PreDestroy 131 | public void preDestroy() { 132 | try { 133 | zipFile.close(); 134 | } catch (IOException ex) { 135 | Log.errorf(ex, "Error closing 3mf: %s", ex.getMessage()); 136 | } 137 | } 138 | 139 | public InputStream getStream() throws FileNotFoundException { 140 | return new FileInputStream(file); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/bambu/view/dashboard/Dashboard.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.view.dashboard; 2 | 3 | import com.tfyre.bambu.BambuConfig; 4 | import com.tfyre.bambu.printer.BambuPrinter; 5 | import com.tfyre.bambu.MainLayout; 6 | import com.tfyre.bambu.SystemRoles; 7 | import com.vaadin.flow.component.AttachEvent; 8 | import com.vaadin.flow.component.Component; 9 | import com.vaadin.flow.component.UI; 10 | import com.vaadin.flow.router.PageTitle; 11 | import com.vaadin.flow.router.Route; 12 | import jakarta.inject.Inject; 13 | import java.util.ArrayList; 14 | import java.util.Comparator; 15 | import java.util.List; 16 | import java.util.function.Consumer; 17 | import com.tfyre.bambu.printer.BambuPrinters; 18 | import com.tfyre.bambu.view.PushDiv; 19 | import jakarta.annotation.security.RolesAllowed; 20 | import jakarta.enterprise.inject.Instance; 21 | 22 | /** 23 | * 24 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 25 | */ 26 | @Route(value = "", layout = MainLayout.class) 27 | @PageTitle("Dashboard") 28 | @RolesAllowed({ SystemRoles.ROLE_ADMIN, SystemRoles.ROLE_NORMAL }) 29 | public class Dashboard extends PushDiv { 30 | 31 | @Inject 32 | BambuPrinters printers; 33 | @Inject 34 | Instance cardInstance; 35 | @Inject 36 | BambuConfig config; 37 | 38 | @Override 39 | protected void onAttach(final AttachEvent attachEvent) { 40 | super.onAttach(attachEvent); 41 | final List runnables = new ArrayList<>(); 42 | addClassName("dashboard-view"); 43 | 44 | printers.getPrinters() 45 | .stream().sorted(Comparator.comparing(BambuPrinter::getName)) 46 | .map(printer -> handlePrinter(printer, runnables::add)) 47 | .forEach(this::add); 48 | final UI ui = attachEvent.getUI(); 49 | createFuture(() -> ui.access(() -> runnables.forEach(Runnable::run)), config.refreshInterval()); 50 | } 51 | 52 | private Component handlePrinter(final BambuPrinter printer, final Consumer consumer) { 53 | final DashboardPrinter card = cardInstance.get(); 54 | consumer.accept(card::update); 55 | return card.build(printer, true); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/ftp/BambuFtp.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.ftp; 2 | 3 | import com.tfyre.bambu.BambuConfig; 4 | import com.tfyre.bambu.printer.BambuPrinters; 5 | import io.quarkus.logging.Log; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.net.Socket; 9 | import java.net.URI; 10 | import javax.net.ssl.SSLContext; 11 | import org.apache.commons.net.ProtocolCommandEvent; 12 | import org.apache.commons.net.ProtocolCommandListener; 13 | import org.apache.commons.net.ftp.FTP; 14 | import org.bouncycastle.jsse.BCExtendedSSLSession; 15 | import org.bouncycastle.jsse.BCSSLSocket; 16 | 17 | /** 18 | * FTPS Client with SSL Session Reuse. 19 | * 20 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 21 | */ 22 | public class BambuFtp extends org.apache.commons.net.ftp.FTPSClient { 23 | 24 | private final boolean useBC; 25 | private BambuConfig.Printer config; 26 | private URI uri; 27 | 28 | private BambuFtp(final boolean useBC, final SSLContext context) { 29 | super(true, context); 30 | this.useBC = useBC; 31 | } 32 | 33 | public BambuFtp() { 34 | this(false, null); 35 | } 36 | 37 | public BambuFtp(final SSLContext context) { 38 | this(true, context); 39 | } 40 | 41 | @Override 42 | protected void _prepareDataSocket_(Socket dataSocket) { 43 | if (!useBC) { 44 | return; 45 | } 46 | if (_socket_ instanceof BCSSLSocket sslSocket) { 47 | final BCExtendedSSLSession bcSession = sslSocket.getBCSession(); 48 | if (bcSession != null && bcSession.isValid() && dataSocket instanceof BCSSLSocket dataSslSocket) { 49 | dataSslSocket.setBCSessionToResume(bcSession); 50 | dataSslSocket.setHost(bcSession.getPeerHost()); 51 | } 52 | } 53 | } 54 | 55 | private ProtocolCommandListener getListener(final String name) { 56 | return new ProtocolCommandListener() { 57 | 58 | private void log(ProtocolCommandEvent event) { 59 | Log.infof("%s: command[%s] message[%s]", name, event.getCommand(), event.getMessage().trim()); 60 | } 61 | 62 | @Override 63 | public void protocolCommandSent(ProtocolCommandEvent event) { 64 | log(event); 65 | } 66 | 67 | @Override 68 | public void protocolReplyReceived(ProtocolCommandEvent event) { 69 | log(event); 70 | } 71 | }; 72 | } 73 | 74 | private URI getURI() { 75 | return URI.create(config.ftp().url().orElseGet(() -> "ftps://%s:%d".formatted(config.ip(), config.ftp().port()))); 76 | } 77 | 78 | public BambuFtp setup(final BambuPrinters.PrinterDetail printer, final FTPEventListener listener) { 79 | setCopyStreamListener(listener); 80 | config = printer.config(); 81 | if (config.ftp().logCommands()) { 82 | addProtocolCommandListener(getListener(printer.name())); 83 | } 84 | setStrictReplyParsing(false); 85 | uri = getURI(); 86 | return this; 87 | } 88 | 89 | public void doConnect() throws IOException { 90 | if (!isConnected()) { 91 | connect(uri.getHost(), uri.getPort()); 92 | } 93 | } 94 | 95 | public boolean doLogin() throws IOException { 96 | if (!login(config.username(), config.accessCode())) { 97 | return false; 98 | } 99 | 100 | execPROT("P"); 101 | enterLocalPassiveMode(); 102 | return true; 103 | } 104 | 105 | public boolean doUpload(final String fileName, final InputStream inputStream) throws IOException { 106 | deleteFile(fileName); 107 | setFileType(FTP.BINARY_FILE_TYPE); 108 | return storeFile(fileName, inputStream); 109 | } 110 | 111 | public void doClose() throws IOException { 112 | quit(); 113 | disconnect(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/ftp/FTPEventListener.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.ftp; 2 | 3 | import org.apache.commons.net.io.CopyStreamEvent; 4 | import org.apache.commons.net.io.CopyStreamListener; 5 | 6 | /** 7 | * 8 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 9 | */ 10 | public interface FTPEventListener extends CopyStreamListener { 11 | 12 | @Override 13 | default void bytesTransferred(final CopyStreamEvent event) { 14 | bytesTransferred(event.getTotalBytesTransferred(), event.getBytesTransferred(), event.getStreamSize()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/ftp/FTPSClientProvider.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.ftp; 2 | 3 | import com.tfyre.bambu.BambuConfig; 4 | import com.tfyre.bambu.ssl.NoopTrustSocketFactory; 5 | import io.quarkus.logging.Log; 6 | import jakarta.annotation.PostConstruct; 7 | import jakarta.enterprise.context.ApplicationScoped; 8 | import jakarta.enterprise.context.Dependent; 9 | import jakarta.enterprise.inject.Produces; 10 | import jakarta.inject.Inject; 11 | import java.security.KeyManagementException; 12 | import java.security.NoSuchAlgorithmException; 13 | import java.security.NoSuchProviderException; 14 | import java.security.SecureRandom; 15 | import java.security.Security; 16 | import javax.net.ssl.SSLContext; 17 | import javax.net.ssl.TrustManager; 18 | import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; 19 | 20 | /** 21 | * 22 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 23 | */ 24 | @ApplicationScoped 25 | public class FTPSClientProvider { 26 | 27 | private static final String SETTING = "jdk.tls.useExtendedMasterSecret"; 28 | 29 | @Inject 30 | BambuConfig config; 31 | @Inject 32 | NoopTrustSocketFactory noopTrustSocketFactory; 33 | 34 | private SSLContext sslContext; 35 | 36 | @PostConstruct 37 | public void postConstruct() throws KeyManagementException, NoSuchAlgorithmException, NoSuchProviderException { 38 | if (!config.useBouncyCastle()) { 39 | Log.info("BouncyCastle disabled"); 40 | return; 41 | } 42 | Log.infof("BouncyCastle enabled: %s=[%s]", SETTING, System.getProperty(SETTING)); 43 | 44 | Security.addProvider(new BouncyCastleJsseProvider()); 45 | sslContext = SSLContext.getInstance("TLSv1.2", "BCJSSE"); 46 | sslContext.init(null, new TrustManager[]{noopTrustSocketFactory.createNoopTrustManager()}, new SecureRandom()); // 1 47 | } 48 | 49 | @Produces 50 | @Dependent 51 | public BambuFtp provide() { 52 | if (!config.useBouncyCastle()) { 53 | return new BambuFtp(); 54 | } 55 | return new BambuFtp(sslContext); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/servlet/TFyreIdentityManager.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.servlet; 2 | 3 | import io.quarkus.arc.Unremovable; 4 | import io.quarkus.logging.Log; 5 | import io.quarkus.security.credential.PasswordCredential; 6 | import io.quarkus.security.identity.IdentityProviderManager; 7 | import io.quarkus.security.identity.request.AuthenticationRequest; 8 | import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; 9 | import io.quarkus.undertow.runtime.QuarkusUndertowAccount; 10 | import io.undertow.security.idm.Account; 11 | import io.undertow.security.idm.Credential; 12 | import io.undertow.security.idm.IdentityManager; 13 | import jakarta.annotation.Priority; 14 | import jakarta.enterprise.inject.Alternative; 15 | import jakarta.inject.Inject; 16 | import jakarta.inject.Singleton; 17 | import jakarta.transaction.Transactional; 18 | 19 | /** 20 | * 21 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 22 | */ 23 | @Singleton 24 | @Unremovable 25 | @Alternative 26 | @Priority(100) 27 | public class TFyreIdentityManager implements IdentityManager { 28 | 29 | @Inject 30 | IdentityProviderManager ipm; 31 | 32 | @Override 33 | public Account verify(final Account account) { 34 | Log.debugf("verify1: %s", account.getPrincipal().getName()); 35 | return account; 36 | } 37 | 38 | @Transactional 39 | QuarkusUndertowAccount authenticateBlocking(final AuthenticationRequest authenticationRequest) { 40 | return new QuarkusUndertowAccount(ipm.authenticateBlocking(authenticationRequest)); 41 | } 42 | 43 | @Override 44 | public Account verify(final String id, final Credential credential) { 45 | Log.debugf("verify2: %s - %s", id, credential.getClass()); 46 | 47 | if (credential instanceof io.undertow.security.idm.PasswordCredential password) { 48 | return authenticateBlocking(new UsernamePasswordAuthenticationRequest(id, new PasswordCredential(password.getPassword()))); 49 | } 50 | 51 | return null; 52 | } 53 | 54 | @Override 55 | public Account verify(final Credential credential) { 56 | Log.debugf("verify3: %s", credential.getClass()); 57 | return null; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/servlet/TFyreIdentityProvider.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.servlet; 2 | 3 | import com.tfyre.bambu.BambuConfig; 4 | import com.tfyre.bambu.SystemRoles; 5 | import io.quarkus.elytron.security.common.BcryptUtil; 6 | import io.quarkus.security.AuthenticationFailedException; 7 | import io.quarkus.security.identity.AuthenticationRequestContext; 8 | import io.quarkus.security.identity.IdentityProvider; 9 | import io.quarkus.security.identity.SecurityIdentity; 10 | import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; 11 | import io.quarkus.security.runtime.QuarkusPrincipal; 12 | import io.quarkus.security.runtime.QuarkusSecurityIdentity; 13 | import io.smallrye.mutiny.Uni; 14 | import jakarta.annotation.PostConstruct; 15 | import jakarta.inject.Inject; 16 | import jakarta.inject.Singleton; 17 | import java.security.Security; 18 | import java.security.spec.InvalidKeySpecException; 19 | import java.util.HashMap; 20 | import java.util.HashSet; 21 | import java.util.Map; 22 | import java.util.Optional; 23 | import java.util.Set; 24 | import java.util.stream.Collectors; 25 | import org.wildfly.security.credential.PasswordCredential; 26 | import org.wildfly.security.evidence.PasswordGuessEvidence; 27 | import org.wildfly.security.password.util.ModularCrypt; 28 | 29 | /** 30 | * 31 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 32 | */ 33 | @Singleton 34 | public class TFyreIdentityProvider implements IdentityProvider { 35 | 36 | @Inject 37 | BambuConfig config; 38 | 39 | private final Map map = new HashMap<>(); 40 | 41 | @PostConstruct 42 | public void postConstruct() { 43 | map.clear(); 44 | if (config.autoLogin()) { 45 | final String userPass = SystemRoles.USER_ADMIN.toLowerCase(); 46 | map.put(userPass, new User(BcryptUtil.bcryptHash(userPass), SystemRoles.ROLE_ADMIN)); 47 | return; 48 | } 49 | final Set seen = new HashSet<>(); 50 | map.putAll(config.users().entrySet() 51 | .stream() 52 | .filter(e -> seen.add(e.getKey().toLowerCase())) 53 | .collect(Collectors.toMap(e -> e.getKey().toLowerCase(), e -> { 54 | String password = e.getValue().password(); 55 | if (ModularCrypt.identifyAlgorithm(password.toCharArray()) == null) { 56 | password = BcryptUtil.bcryptHash(password); 57 | } 58 | return new User(password, e.getValue().role()); 59 | })) 60 | ); 61 | } 62 | 63 | @Override 64 | public Class getRequestType() { 65 | return UsernamePasswordAuthenticationRequest.class; 66 | } 67 | 68 | @Override 69 | public Uni authenticate(final UsernamePasswordAuthenticationRequest request, final AuthenticationRequestContext context) { 70 | return context.runBlocking(() -> 71 | Optional.ofNullable(map.get(request.getUsername().toLowerCase())) 72 | .filter(u -> passwordValid(u.password, request.getPassword().getPassword())) 73 | .map(u -> QuarkusSecurityIdentity.builder() 74 | .setPrincipal(new QuarkusPrincipal(request.getUsername())) 75 | .addCredential(request.getPassword()) 76 | .addRole(u.role()) 77 | .build() 78 | ) 79 | .orElseThrow(AuthenticationFailedException::new) 80 | ); 81 | } 82 | 83 | private boolean passwordValid(final String password, final char[] requestPassword) { 84 | final PasswordGuessEvidence evidence = new PasswordGuessEvidence(requestPassword); 85 | try { 86 | return new PasswordCredential(ModularCrypt.decode(password)).verify(Security::getProviders, evidence); 87 | } catch (InvalidKeySpecException ex) { 88 | return false; 89 | } 90 | } 91 | 92 | private record User(String password, String role) { 93 | 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /bambu/src/main/java/com/tfyre/servlet/TFyreServletExtension.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.servlet; 2 | 3 | import io.undertow.servlet.ServletExtension; 4 | import io.undertow.servlet.api.DeploymentInfo; 5 | import jakarta.enterprise.inject.spi.CDI; 6 | import jakarta.servlet.ServletContext; 7 | 8 | /** 9 | * 10 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 11 | */ 12 | public class TFyreServletExtension implements ServletExtension { 13 | 14 | @Override 15 | public void handleDeployment(final DeploymentInfo deploymentInfo, final ServletContext servletContext) { 16 | deploymentInfo.setIdentityManager(CDI.current().select(TFyreIdentityManager.class).get()); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/beans.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/resources/bambu/README.md: -------------------------------------------------------------------------------- 1 | Images from https://github.com/SoftFever/OrcaSlicer/tree/main/resources/images 2 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/resources/bambu/ams_humidity_0.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/resources/bambu/ams_humidity_1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/resources/bambu/ams_humidity_2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/resources/bambu/ams_humidity_3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/resources/bambu/ams_humidity_4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/resources/bambu/fetchAll.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | PREFIX=https://raw.githubusercontent.com/SoftFever/OrcaSlicer/main/resources/images/ 4 | 5 | 6 | fetch() { 7 | FILE=${1} 8 | echo Fetching ${FILE} 9 | curl -Ss --fail -o ${FILE} ${PREFIX}${FILE} 10 | } 11 | 12 | fetch monitor_lamp_off.svg 13 | fetch monitor_lamp_on.svg 14 | fetch monitor_bed_temp.svg 15 | fetch monitor_bed_temp_active.svg 16 | fetch monitor_nozzle_temp.svg 17 | fetch monitor_nozzle_temp_active.svg 18 | fetch monitor_frame_temp.svg 19 | fetch monitor_speed.svg 20 | fetch monitor_speed_active.svg 21 | fetch ams_humidity_0.svg 22 | fetch ams_humidity_1.svg 23 | fetch ams_humidity_2.svg 24 | fetch ams_humidity_3.svg 25 | fetch ams_humidity_4.svg 26 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/resources/bambu/monitor_bed_temp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/resources/bambu/monitor_bed_temp_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/resources/bambu/monitor_frame_temp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/resources/bambu/monitor_lamp_off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/resources/bambu/monitor_lamp_on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/resources/bambu/monitor_nozzle_temp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/resources/bambu/monitor_nozzle_temp_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/resources/bambu/monitor_speed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/resources/bambu/monitor_speed_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener: -------------------------------------------------------------------------------- 1 | com.tfyre.bambu.security.NavigationAccessCheckerInitializer -------------------------------------------------------------------------------- /bambu/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension: -------------------------------------------------------------------------------- 1 | com.tfyre.servlet.TFyreServletExtension -------------------------------------------------------------------------------- /bambu/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | %dev.quarkus.mqtt.devservices.enabled=false 2 | 3 | quarkus.scheduler.start-mode=forced 4 | quarkus.http.enable-compression=true 5 | quarkus.http.limits.max-body-size=20M 6 | 7 | quarkus.rest-client.cloud.url=https://bambulab.com/ 8 | -------------------------------------------------------------------------------- /common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.tfyre.bambu 6 | bambu-parent 7 | 1.7.0 8 | 9 | bambu-common 10 | jar 11 | 12 | 13 | org.apache.camel.quarkus 14 | camel-quarkus-paho 15 | 16 | 17 | org.apache.camel.quarkus 18 | camel-quarkus-direct 19 | 20 | 21 | io.quarkus 22 | quarkus-grpc 23 | 24 | 25 | com.google.protobuf 26 | protobuf-java-util 27 | 28 | 29 | 30 | package quarkus:dev 31 | 32 | 33 | org.codehaus.mojo 34 | jaxb2-maven-plugin 35 | 36 | 37 | xjc 38 | generate-sources 39 | 40 | xjc 41 | 42 | 43 | 44 | true 45 | 46 | ${project.basedir}/src/main/schema/schema.xjb 47 | 48 | 49 | ${project.basedir}/src/main/schema/slice-info-1.0.xsd 50 | 51 | ${project.basedir}/src/main/schema/catalog.xml 52 | ${project.build.directory}/generated-sources/schema 53 | true 54 | true 55 | 56 | fluent-api 57 | default-value 58 | 59 | 60 | 61 | 62 | 63 | 64 | io.quarkus 65 | quarkus-maven-plugin 66 | true 67 | 68 | 69 | 70 | build 71 | generate-code 72 | generate-code-tests 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /common/src/main/java/com/tfyre/bambu/mqtt/AbstractMqttController.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.mqtt; 2 | 3 | import com.tfyre.bambu.ssl.NoopTrustSocketFactory; 4 | import java.security.SecureRandom; 5 | import java.util.Optional; 6 | import java.util.Random; 7 | import org.apache.camel.Endpoint; 8 | import org.apache.camel.builder.RouteBuilder; 9 | 10 | /** 11 | * 12 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 13 | */ 14 | public abstract class AbstractMqttController extends RouteBuilder { 15 | 16 | private final Random rnd = new SecureRandom(); 17 | 18 | private String newClientId() { 19 | return "camel-paho-%s".formatted(Long.toHexString(Math.abs(rnd.nextLong()))); 20 | } 21 | 22 | protected Endpoint getPrinterEndpoint(final String name) { 23 | return endpoint("direct:bambu-%s".formatted(name)); 24 | } 25 | 26 | protected Endpoint getMqttEndpoint(final String topic, final String url, final String username, final String password) { 27 | return endpoint("paho:%s?brokerUrl=%s&userName=%s&password=%s&qos=0&lazyStartProducer=true&socketFactory=#%s&clientId=%s" 28 | .formatted(topic, url, username, password, NoopTrustSocketFactory.FACTORY, newClientId())); 29 | } 30 | 31 | protected String getTopic(final Optional topic, final String deviceId, final String type) { 32 | return topic.orElseGet(() -> "device/%s/%s".formatted(deviceId, type)); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /common/src/main/java/com/tfyre/bambu/ssl/NoopTrustSocketFactory.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.ssl; 2 | 3 | import io.quarkus.logging.Log; 4 | import jakarta.annotation.PostConstruct; 5 | import jakarta.enterprise.context.ApplicationScoped; 6 | import jakarta.enterprise.inject.Produces; 7 | import jakarta.inject.Named; 8 | import java.net.Socket; 9 | import java.security.KeyManagementException; 10 | import java.security.NoSuchAlgorithmException; 11 | import java.security.cert.CertificateException; 12 | import java.security.cert.X509Certificate; 13 | import javax.net.SocketFactory; 14 | import javax.net.ssl.SSLContext; 15 | import javax.net.ssl.SSLEngine; 16 | import javax.net.ssl.TrustManager; 17 | import javax.net.ssl.X509ExtendedTrustManager; 18 | 19 | /** 20 | * 21 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 22 | */ 23 | @ApplicationScoped 24 | public class NoopTrustSocketFactory { 25 | 26 | public static final String FACTORY = "NoopTrustSocketFactory"; 27 | 28 | 29 | public X509ExtendedTrustManager createNoopTrustManager() { 30 | return new X509ExtendedTrustManager() { 31 | 32 | @Override 33 | public X509Certificate[] getAcceptedIssuers() { 34 | Log.debug("INSECURE: getAcceptedIssuers"); 35 | return null; 36 | } 37 | 38 | @Override 39 | public void checkClientTrusted(X509Certificate[] certs, String authType) { 40 | Log.debugf("INSECURE: checkClientTrusted %s", authType); 41 | } 42 | 43 | @Override 44 | public void checkServerTrusted(X509Certificate[] certs, String authType) { 45 | Log.debugf("INSECURE: checkServerTrusted %s", authType); 46 | } 47 | 48 | @Override 49 | public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { 50 | Log.debugf("INSECURE: checkClientTrusted %s", authType); 51 | } 52 | 53 | @Override 54 | public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { 55 | Log.debugf("INSECURE: checkServerTrusted %s", authType); 56 | } 57 | 58 | @Override 59 | public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { 60 | Log.debugf("INSECURE: checkClientTrusted %s", authType); 61 | } 62 | 63 | @Override 64 | public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { 65 | Log.debugf("INSECURE: checkServerTrusted %s", authType); 66 | } 67 | }; 68 | } 69 | 70 | @Produces 71 | @Named(FACTORY) 72 | public SocketFactory createSocketFactory() throws NoSuchAlgorithmException, KeyManagementException { 73 | final SSLContext sc = SSLContext.getInstance("ssl"); 74 | sc.init(null, new TrustManager[]{ createNoopTrustManager() }, null); 75 | return sc.getSocketFactory(); 76 | } 77 | 78 | @PostConstruct 79 | public void postConstruct() { 80 | Log.errorf("Using INSECURE %s", getClass().getName()); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /common/src/main/resources/META-INF/beans.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /common/src/main/schema/catalog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /common/src/main/schema/schema.xjb: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /common/src/main/schema/slice-info-1.0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Slice Info 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /common/src/main/schema/xml.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | See http://www.w3.org/XML/1998/namespace.html and 7 | http://www.w3.org/TR/REC-xml for information about this namespace. 8 | 9 | This schema document describes the XML namespace, in a form 10 | suitable for import by other schema documents. 11 | 12 | Note that local names in this namespace are intended to be defined 13 | only by the World Wide Web Consortium or its subgroups. The 14 | following names are currently defined in this namespace and should 15 | not be used with conflicting semantics by any Working Group, 16 | specification, or document instance: 17 | 18 | base (as an attribute name): denotes an attribute whose value 19 | provides a URI to be used as the base for interpreting any 20 | relative URIs in the scope of the element on which it 21 | appears; its value is inherited. This name is reserved 22 | by virtue of its definition in the XML Base specification. 23 | 24 | id (as an attribute name): denotes an attribute whose value 25 | should be interpreted as if declared to be of type ID. 26 | The xml:id specification is not yet a W3C Recommendation, 27 | but this attribute is included here to facilitate experimentation 28 | with the mechanisms it proposes. Note that it is _not_ included 29 | in the specialAttrs attribute group. 30 | 31 | lang (as an attribute name): denotes an attribute whose value 32 | is a language code for the natural language of the content of 33 | any element; its value is inherited. This name is reserved 34 | by virtue of its definition in the XML specification. 35 | 36 | space (as an attribute name): denotes an attribute whose 37 | value is a keyword indicating what whitespace processing 38 | discipline is intended for the content of the element; its 39 | value is inherited. This name is reserved by virtue of its 40 | definition in the XML specification. 41 | 42 | Father (in any context at all): denotes Jon Bosak, the chair of 43 | the original XML Working Group. This name is reserved by 44 | the following decision of the W3C XML Plenary and 45 | XML Coordination groups: 46 | 47 | In appreciation for his vision, leadership and dedication 48 | the W3C XML Plenary on this 10th day of February, 2000 49 | reserves for Jon Bosak in perpetuity the XML name 50 | xml:Father 51 | 52 | 53 | 54 | 55 | This schema defines attributes and an attribute group 56 | suitable for use by 57 | schemas wishing to allow xml:base, xml:lang, xml:space or xml:id 58 | attributes on elements they define. 59 | 60 | To enable this, such a schema must import this schema 61 | for the XML namespace, e.g. as follows: 62 | <schema . . .> 63 | . . . 64 | <import namespace="http://www.w3.org/XML/1998/namespace" 65 | schemaLocation="http://www.w3.org/2005/08/xml.xsd"/> 66 | 67 | Subsequently, qualified reference to any of the attributes 68 | or the group defined below will have the desired effect, e.g. 69 | 70 | <type . . .> 71 | . . . 72 | <attributeGroup ref="xml:specialAttrs"/> 73 | 74 | will define a type which will schema-validate an instance 75 | element with any of those attributes 76 | 77 | 78 | 79 | 80 | In keeping with the XML Schema WG's standard versioning 81 | policy, this schema document will persist at 82 | http://www.w3.org/2005/08/xml.xsd. 83 | At the date of issue it can also be found at 84 | http://www.w3.org/2001/xml.xsd. 85 | The schema document at that URI may however change in the future, 86 | in order to remain compatible with the latest version of XML Schema 87 | itself, or with the XML namespace itself. In other words, if the XML 88 | Schema or XML namespaces change, the version of this document at 89 | http://www.w3.org/2001/xml.xsd will change 90 | accordingly; the version at 91 | http://www.w3.org/2005/08/xml.xsd will not change. 92 | 93 | 94 | 95 | 96 | 97 | Attempting to install the relevant ISO 2- and 3-letter 98 | codes as the enumerated possible values is probably never 99 | going to be a realistic possibility. See 100 | RFC 3066 at http://www.ietf.org/rfc/rfc3066.txt and the IANA registry 101 | at http://www.iana.org/assignments/lang-tag-apps.htm for 102 | further information. 103 | 104 | The union allows for the 'un-declaration' of xml:lang with 105 | the empty string. 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | See http://www.w3.org/TR/xmlbase/ for 131 | information about this attribute. 132 | 133 | 134 | 135 | 136 | 137 | 138 | See http://www.w3.org/TR/xml-id/ for 139 | information about this attribute. 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /docker/bambu-liveview/README.md: -------------------------------------------------------------------------------- 1 | # Docker Compose environment for X1C printers 2 | 3 | Docker compose environment for X1C Printers to enable liveview. 4 | 5 | > [!IMPORTANT] 6 | > REMEMBER to enable `LAN Mode Liveview` on the printer 7 | 8 | # MediaMTX Container 9 | 10 | **Copy `example - mediamtx.yml` to `mediamtx.yml`** 11 | 12 | Enable TCP for WebRTC 13 | ```yaml 14 | webrtcLocalTCPAddress: :8189 15 | ``` 16 | 17 | Disable Docker Instance IPs 18 | ```yaml 19 | webrtcIPsFromInterfaces: no 20 | ``` 21 | 22 | Enable Proper IP (docker host OR public ip / dns) 23 | ```yaml 24 | webrtcAdditionalHosts: [10.0.0.456, my.dynamic.dns.com] 25 | ``` 26 | To enable access from internet, add your public ip or DNS **OR** to enable access from local lan, add the ip of the docker host. 27 | 28 | 29 | # Bambu Web 30 | 31 | * Download the [latest](https://github.com/TFyre/bambu-farm/releases/latest) version of bambu-web-X.X.X-runner.jar 32 | 33 | * `bambu-web-env.txt` is the config file instead of normal `.env` 34 | * Update `compose.yml` and replace `bambu-web-X.X.X-runner.jar` with the correct version 35 | 36 | Enable the following in `bambu-web-env.txt`: 37 | ```properties 38 | bambu.live-view-url=/_camerastream/ 39 | 40 | #For Each Printer: 41 | bambu.printers.PRINTER_ID.stream.live-view=true 42 | ``` 43 | 44 | If you have a full custom url for the printer: 45 | ```properties 46 | bambu.printers.PRINTER_ID.stream.url=https://my_stream_domain.com/mystream 47 | ``` 48 | 49 | ## Uploading bigger files 50 | 51 | Add to `.env`: 52 | ```properties 53 | quarkus.http.limits.max-body-size=30M 54 | ``` 55 | 56 | also remember to update `reverse-proxy.conf`: 57 | ```conf 58 | location / { 59 | client_max_body_size 30m; 60 | proxy_pass http://bambuweb; 61 | ``` 62 | 63 | 64 | # Adding your printers 65 | 66 | **Copy `example - compose.yaml` to `compose.yml`** 67 | 68 | Edit `compose.yml` and fix the printers (lines with FIXME) 69 | 70 | ```yaml 71 | printer1: 72 | extends: 73 | file: common-liveview.yml 74 | service: liveview 75 | depends_on: 76 | - mediamtx 77 | environment: 78 | PRINTER_HOST: FIXME_this_is_my_printer_ip_or_host 79 | PRINTER_ID: ([^1])FIXME_this_is_my_printer_id_from_env 80 | PRINTER_ACCESS_CODE: FIXME_this_is_my_printer_access_code 81 | ``` 82 | 83 | > [!NOTE] 84 | > `PRINTER_ID` is the printer id in the `.env` configuration file eg: 85 | > ```properties 86 | > bambu.printers.PRINTER_ID.name=My Printer Name 87 | > ``` 88 | 89 | # Ports required for outside access 90 | 91 | | PORT | UDP/TCP | Purpose | 92 | |--|--|--| 93 | |8189|TCP+UDP|Streaming for WebRTC| 94 | |8080|TCP|HTTP for BambuWeb & WebRTC| 95 | 96 | # Starting 97 | 98 | Start with `docker compose up` 99 | 100 | # Links 101 | 102 | * https://github.com/bluenviron/mediamtx 103 | * https://docs.linuxserver.io/images/docker-ffmpeg/ 104 | -------------------------------------------------------------------------------- /docker/bambu-liveview/common-liveview.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | liveview: 5 | image: linuxserver/ffmpeg 6 | restart: always 7 | environment: 8 | PRINTER_HOST: DONT_TOUCH 9 | PRINTER_USER: bblp 10 | PRINTER_ID: DONT_TOUCH 11 | PRINTER_ACCESS_CODE: DONT_TOUCH 12 | PRINTER_PORT: 322 13 | PRINTER_URL: /streaming/live/1 14 | RTSP_SERVER: mediamtx:8554 15 | entrypoint: 16 | - "bash" 17 | - "-c" 18 | - > 19 | /usr/local/bin/ffmpeg 20 | -timeout 30000000 21 | -re -stream_loop -1 22 | -i rtsps://$${PRINTER_USER}:$${PRINTER_ACCESS_CODE}@$${PRINTER_HOST}:$${PRINTER_PORT}$${PRINTER_URL} 23 | -c:v copy -f rtsp -rtsp_transport tcp 24 | rtsp://$${RTSP_SERVER}/$${PRINTER_ID} 25 | -------------------------------------------------------------------------------- /docker/bambu-liveview/example - compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | mediamtx: 5 | image: bluenviron/mediamtx:1.8.2 6 | restart: always 7 | volumes: 8 | - ./mediamtx.yml:/mediamtx.yml 9 | ports: 10 | #RTSP 11 | #- '8554:8554' 12 | 13 | #WebRTC 14 | - '8189:8189/tcp' 15 | - '8189:8189/udp' 16 | #- '8888:8888' 17 | - '8889:8889' 18 | 19 | bambuweb: 20 | image: azul/zulu-openjdk:21-latest 21 | restart: always 22 | volumes: 23 | - ./bambu-web-X.X.X-runner.jar:/bambu-web-runner.jar 24 | - ./bambu-web-env.txt:/.env 25 | ports: 26 | - '8081:8080' 27 | entrypoint: /usr/bin/java -Djdk.tls.useExtendedMasterSecret=false -jar bambu-web-runner.jar 28 | 29 | nginx: 30 | image: nginx:1.26-alpine 31 | restart: always 32 | volumes: 33 | - ./reverse-proxy.conf:/etc/nginx/conf.d/default.conf 34 | ports: 35 | - '8080:80' 36 | depends_on: 37 | - mediamtx 38 | - bambuweb 39 | 40 | printer1: 41 | extends: 42 | file: common-liveview.yml 43 | service: liveview 44 | depends_on: 45 | - mediamtx 46 | environment: 47 | PRINTER_HOST: FIXME_this_is_my_printer_ip_or_host 48 | PRINTER_ID: FIXME_this_is_my_printer_id 49 | PRINTER_ACCESS_CODE: FIXME_this_is_my_printer_access_code 50 | 51 | # printer2: 52 | # extends: 53 | # file: common-liveview.yml 54 | # service: liveview 55 | # depends_on: 56 | # - mediamtx 57 | # environment: 58 | # PRINTER_HOST: FIXME_this_is_my_printer_ip_or_host 59 | # PRINTER_ID: FIXME_this_is_my_printer_id 60 | # PRINTER_ACCESS_CODE: FIXME_this_is_my_printer_access_code 61 | # 62 | -------------------------------------------------------------------------------- /docker/bambu-liveview/reverse-proxy.conf: -------------------------------------------------------------------------------- 1 | map $http_upgrade $connection_upgrade { 2 | default upgrade; 3 | #'' close; 4 | '' ''; 5 | } 6 | 7 | upstream mediamtx { 8 | keepalive 32; 9 | server mediamtx:8889; 10 | } 11 | 12 | upstream bambuweb { 13 | keepalive 32; 14 | server bambuweb:8080; 15 | } 16 | 17 | server { 18 | listen 80; 19 | listen [::]:80; 20 | server_name reverse-proxy; 21 | access_log off; 22 | root /usr/share/nginx/html; 23 | index index.html index.htm; 24 | 25 | location /_camerastream/ { 26 | rewrite /_camerastream/(.*) /$1 break; 27 | proxy_redirect / /_camerastream/; 28 | absolute_redirect off; 29 | proxy_pass http://mediamtx/; 30 | proxy_http_version 1.1; 31 | proxy_set_header Upgrade $http_upgrade; 32 | proxy_set_header Connection $connection_upgrade; 33 | proxy_set_header Host $host; 34 | # for websockets 35 | proxy_read_timeout 5m; 36 | } 37 | 38 | location / { 39 | client_max_body_size 20m; 40 | proxy_pass http://bambuweb; 41 | proxy_http_version 1.1; 42 | proxy_set_header Upgrade $http_upgrade; 43 | proxy_set_header Connection $connection_upgrade; 44 | proxy_set_header Host $host; 45 | # for websockets 46 | proxy_read_timeout 5m; 47 | } 48 | 49 | error_page 404 /404.html; 50 | location = /40x.html { 51 | } 52 | 53 | error_page 500 502 503 504 /50x.html; 54 | location = /50x.html { 55 | root /usr/share/nginx/html; 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /docker/bambu-local-dev/README.md: -------------------------------------------------------------------------------- 1 | # Certs 2 | password: 1234 3 | ```bash 4 | openssl genrsa -des3 -out ca.key 2048 5 | openssl req -new -x509 -days 1826 -key ca.key -out ca.crt 6 | openssl genrsa -out server.key 2048 7 | openssl req -new -out server.csr -key server.key 8 | openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3600 9 | ``` 10 | # Docker 11 | ```bash 12 | docker compose up 13 | ``` 14 | 15 | # Testing FTP 16 | ```bash 17 | lftp ftps://test2:test2@localhost 18 | debug 19 | set ssl:verify-certificate false 20 | ls 21 | ``` 22 | -------------------------------------------------------------------------------- /docker/bambu-local-dev/compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | mqtt: 5 | image: eclipse-mosquitto 6 | restart: always 7 | volumes: 8 | - ./mqtt/mosquitto.conf:/mosquitto/config/mosquitto.conf 9 | - ./mqtt/password.conf:/mosquitto/config/password.conf 10 | - ./ca.crt:/mosquitto/config/tls-ca.pem 11 | - ./server.crt:/mosquitto/config/tls-cert.pem 12 | - ./server.key:/mosquitto/config/tls-key.pem 13 | ports: 14 | - '8883:8883' 15 | vsftpd: 16 | build: 17 | context: vsftpd 18 | #target: builder 19 | restart: always 20 | volumes: 21 | - ./vsftpd/vsftpd.conf:/etc/vsftpd/vsftpd.conf 22 | - ./vsftpd/users.txt:/etc/vsftpd/virtual_users.txt 23 | - ./server.crt:/etc/vsftpd/vsftpd.crt 24 | - ./server.key:/etc/vsftpd/vsftpd.key 25 | - ./vsftpd/home:/home/vsftpd 26 | ports: 27 | - '990:990' 28 | - '21100-21110:21100-21110' -------------------------------------------------------------------------------- /docker/bambu-local-dev/mqtt/mosquitto.conf: -------------------------------------------------------------------------------- 1 | listener 8883 2 | allow_anonymous false 3 | password_file /mosquitto/config/password.conf 4 | 5 | cafile /mosquitto/config/tls-ca.pem 6 | certfile /mosquitto/config/tls-cert.pem 7 | keyfile /mosquitto/config/tls-key.pem 8 | -------------------------------------------------------------------------------- /docker/bambu-local-dev/mqtt/password.conf: -------------------------------------------------------------------------------- 1 | test1:$7$101$4hui3YQcBoe0l2ww$xYzVFaytTuGaYLqZl6Dzfo1oTiWNFgivWJJ2eLSnwGrcxiIGNaObucfqdp7pTDTe2OOrFFSsYJzhVdBpGSy3rA== 2 | test2:$7$101$fFzAK/6R+wvqKzFD$pjhFUhalX/ZZ1eb7QSu/IgnlaF0n5+mwmKsr1t3SYS924IA1VeqSLgGjAXJ50HaooRwocjcYJD+69Jlh/iNn6g== 3 | test3:$7$101$BbZnSYJqImqvE7sS$FEWiTirdOo6N8vapy6hb6oewVv3/26W0MG/3cow5oMUdLBkvyP+Wk1c6vUj/EurnKS6yuh6iZ6dFHO0L1hP5AQ== 4 | test4:$7$101$ungr5NQHv1qHS380$M+W1z/XW5O/A4y0GDg2ugXhB5G6uTvYucDgh9hNoV26f/Ok6xF3tvqBM4Z/sR0KD3TLUOJpPZkucqz8IxvAelg== 5 | -------------------------------------------------------------------------------- /docker/bambu-local-dev/vsftpd/Dockerfile: -------------------------------------------------------------------------------- 1 | #Copied from https://github.com/fauria/docker-vsftpd/blob/master/run-vsftpd.sh 2 | FROM centos:7 3 | 4 | ARG USER_ID=14 5 | ARG GROUP_ID=50 6 | 7 | RUN yum -y update \ 8 | && yum clean all \ 9 | && yum install -y vsftpd db4-utils db4 iproute \ 10 | && yum clean all 11 | 12 | RUN usermod -u ${USER_ID} ftp 13 | RUN groupmod -g ${GROUP_ID} ftp 14 | 15 | COPY vsftpd.conf /etc/vsftpd/ 16 | COPY vsftpd_virtual /etc/pam.d/ 17 | COPY run-vsftpd.sh /usr/sbin/ 18 | 19 | RUN chmod +x /usr/sbin/run-vsftpd.sh 20 | 21 | VOLUME /home/vsftpd 22 | VOLUME /var/log/vsftpd 23 | 24 | #EXPOSE 990 25 | 26 | CMD ["/usr/sbin/run-vsftpd.sh"] -------------------------------------------------------------------------------- /docker/bambu-local-dev/vsftpd/run-vsftpd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | /usr/bin/db_load -T -t hash -f /etc/vsftpd/virtual_users.txt /etc/vsftpd/virtual_users.db 3 | 4 | &>/dev/null /usr/sbin/vsftpd /etc/vsftpd/vsftpd.conf -------------------------------------------------------------------------------- /docker/bambu-local-dev/vsftpd/users.txt: -------------------------------------------------------------------------------- 1 | test1 2 | test1 3 | test2 4 | test2 5 | test3 6 | test3 7 | test4 8 | test4 9 | -------------------------------------------------------------------------------- /docker/bambu-local-dev/vsftpd/vsftpd.conf: -------------------------------------------------------------------------------- 1 | # Run in the foreground to keep the container running: 2 | background=NO 3 | 4 | # Allow anonymous FTP? (Beware - allowed by default if you comment this out). 5 | anonymous_enable=NO 6 | 7 | # Uncomment this to allow local users to log in. 8 | local_enable=YES 9 | 10 | ## Enable virtual users 11 | guest_enable=YES 12 | 13 | ## Virtual users will use the same permissions as anonymous 14 | virtual_use_local_privs=YES 15 | 16 | # Uncomment this to enable any form of FTP write command. 17 | write_enable=YES 18 | 19 | ## PAM file name 20 | pam_service_name=vsftpd_virtual 21 | 22 | ## Home Directory for virtual users 23 | user_sub_token=$USER 24 | local_root=/home/vsftpd/$USER 25 | 26 | # You may specify an explicit list of local users to chroot() to their home 27 | # directory. If chroot_local_user is YES, then this list becomes a list of 28 | # users to NOT chroot(). 29 | chroot_local_user=YES 30 | 31 | # Workaround chroot check. 32 | # See https://www.benscobie.com/fixing-500-oops-vsftpd-refusing-to-run-with-writable-root-inside-chroot/ 33 | # and http://serverfault.com/questions/362619/why-is-the-chroot-local-user-of-vsftpd-insecure 34 | allow_writeable_chroot=YES 35 | 36 | ## Hide ids from user 37 | hide_ids=YES 38 | 39 | ## Enable logging 40 | xferlog_enable=YES 41 | xferlog_file=/var/log/vsftpd/vsftpd.log 42 | 43 | ## Enable active mode 44 | port_enable=YES 45 | connect_from_port_20=YES 46 | ftp_data_port=20 47 | 48 | ## Disable seccomp filter sanboxing 49 | seccomp_sandbox=NO 50 | 51 | 52 | ssl_enable=YES 53 | force_local_data_ssl=YES 54 | force_local_logins_ssl=YES 55 | ssl_tlsv1_1=YES 56 | ssl_tlsv1_2=YES 57 | ssl_tlsv1=NO 58 | ssl_sslv2=NO 59 | ssl_sslv3=NO 60 | #X1C 61 | #require_ssl_reuse=YES 62 | #Others 63 | require_ssl_reuse=NO 64 | ssl_ciphers=HIGH 65 | rsa_cert_file=/etc/vsftpd/vsftpd.crt 66 | rsa_private_key_file=/etc/vsftpd/vsftpd.key 67 | implicit_ssl=YES 68 | listen_port=990 69 | log_ftp_protocol=YES 70 | vsftpd_log_file=/var/log/vsftpd/vsftpd.log 71 | 72 | 73 | pasv_address=172.17.0.1 74 | pasv_max_port=21110 75 | pasv_min_port=21100 76 | pasv_addr_resolve=NO 77 | pasv_enable=YES 78 | file_open_mode=0666 79 | local_umask=077 80 | xferlog_std_format=yes 81 | reverse_lookup_enable=YES 82 | pasv_promiscuous=NO 83 | port_promiscuous=NO 84 | -------------------------------------------------------------------------------- /docker/bambu-local-dev/vsftpd/vsftpd_virtual: -------------------------------------------------------------------------------- 1 | #%PAM-1.0 2 | auth required pam_userdb.so db=/etc/vsftpd/virtual_users 3 | account required pam_userdb.so db=/etc/vsftpd/virtual_users 4 | session required pam_loginuid.so 5 | -------------------------------------------------------------------------------- /docs/README.service.md: -------------------------------------------------------------------------------- 1 | # Runnig as a linux service 2 | 3 | All commands should be executed as `root` 4 | 5 | Ensure you have Java 21 running 6 | ```bash 7 | root@raspberrypi:~# java --version 8 | 9 | openjdk 21.0.2 2024-01-16 LTS 10 | OpenJDK Runtime Environment Zulu21.32+17-CA (build 21.0.2+13-LTS) 11 | OpenJDK 64-Bit Server VM Zulu21.32+17-CA (build 21.0.2+13-LTS, mixed mode, sharing) 12 | ``` 13 | 14 | Create a new user & setup folder 15 | ```bash 16 | export BAMBU_USER=bambu-web 17 | #create the user without shell 18 | useradd -m --shell=/bin/false ${BAMBU_USER} 19 | #create service folder 20 | mkdir -p /usr/local/${BAMBU_USER} 21 | #setup permissions 22 | chown ${BAMBU_USER}.${BAMBU_USER} /usr/local/${BAMBU_USER} 23 | ``` 24 | 25 | Download / Update the bambu-web version 26 | ```bash 27 | export BAMBU_USER=bambu-web 28 | #login as user 29 | su ${BAMBU_USER} -s /bin/bash 30 | #change to service folder 31 | cd /usr/local/${BAMBU_USER} 32 | #grab the latest version 33 | export BAMBU_URL=$(curl -s https://api.github.com/repos/tfyre/bambu-farm/releases/latest \ 34 | | grep browser_download_url | cut -d'"' -f4) 35 | curl -LO ${BAMBU_URL} 36 | ln -sf $(basename ${BAMBU_URL}) bambu-web-latest-runner.jar 37 | exit 38 | ``` 39 | 40 | Create/edit the config file using your favorite editor (eg vim) 41 | ```bash 42 | export BAMBU_USER=bambu-web 43 | #login as user 44 | su ${BAMBU_USER} -s /bin/bash 45 | #change to service folder 46 | cd /usr/local/${BAMBU_USER} 47 | vim .env 48 | ``` 49 | 50 | Create & start the service 51 | ```bash 52 | export BAMBU_USER=bambu-web 53 | # Create the service file 54 | cat < /etc/systemd/system/${BAMBU_USER}.service 55 | [Unit] 56 | Description=Bambu Web Service 57 | Requires=network.target remote-fs.target 58 | After=network.target remote-fs.target 59 | 60 | [Service] 61 | Type=simple 62 | User=${BAMBU_USER} 63 | Group=${BAMBU_USER} 64 | Restart=always 65 | RestartSec=30 66 | ExecStart=java -jar bambu-web-latest-runner.jar 67 | WorkingDirectory=/usr/local/${BAMBU_USER} 68 | SuccessExitStatus=143 69 | 70 | [Install] 71 | WantedBy=multi-user.target 72 | EOF 73 | 74 | #enable the service for system startup 75 | systemctl enable ${BAMBU_USER} 76 | #start the service 77 | systemctl start ${BAMBU_USER} 78 | #check the status 79 | systemctl status ${BAMBU_USER} 80 | ``` 81 | 82 | # Removing the service 83 | 84 | ```bash 85 | export BAMBU_USER=bambu-web 86 | #stop the service 87 | systemctl stop ${BAMBU_USER} 88 | #disable the service 89 | systemctl disable ${BAMBU_USER} 90 | #remove the service 91 | rm -f /etc/systemd/system/${BAMBU_USER}.service 92 | 93 | #remove the service folder 94 | rm -fr /usr/local/${BAMBU_USER} 95 | #remove the user 96 | userdel -r ${BAMBU_USER} 97 | ``` 98 | 99 | -------------------------------------------------------------------------------- /docs/bambufarm1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TFyre/bambu-farm/c788104297e79502aa14825572b9ecc3762676c4/docs/bambufarm1.jpg -------------------------------------------------------------------------------- /docs/bambufarm2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TFyre/bambu-farm/c788104297e79502aa14825572b9ecc3762676c4/docs/bambufarm2.jpg -------------------------------------------------------------------------------- /docs/bambufarm3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TFyre/bambu-farm/c788104297e79502aa14825572b9ecc3762676c4/docs/bambufarm3.jpg -------------------------------------------------------------------------------- /docs/batchprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TFyre/bambu-farm/c788104297e79502aa14825572b9ecc3762676c4/docs/batchprint.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.tfyre.bambu 5 | bambu-parent 6 | 1.7.0 7 | pom 8 | 9 | common 10 | bambu 11 | server 12 | 13 | 14 | UTF-8 15 | UTF-8 16 | 21 17 | true 18 | true 19 | 20 | false 21 | 24.7.5 22 | 3.23.0 23 | 3.14.0 24 | 5.17.0 25 | 3.11.1 26 | 3.3.1 27 | 28 | 3.3.0 29 | 4.0.5 30 | 3.0 31 | 1.1 32 | 33 | 34 | 35 | 36 | io.quarkus 37 | quarkus-bom 38 | ${quarkus.version} 39 | pom 40 | import 41 | 42 | 43 | 44 | io.quarkus.platform 45 | quarkus-camel-bom 46 | ${quarkus.version} 47 | pom 48 | import 49 | 50 | 51 | 52 | com.vaadin 53 | vaadin-bom 54 | pom 55 | import 56 | ${vaadin.version} 57 | 58 | 59 | com.vaadin 60 | vaadin-quarkus-extension 61 | ${vaadin.version} 62 | 63 | 64 | 65 | com.tfyre.quarkus 66 | quarkus-common-vaadin 67 | ${vaadin-common} 68 | 69 | 70 | 71 | 72 | 73 | net.java.dev.jna 74 | jna 75 | ${jna.version} 76 | 77 | 78 | net.java.dev.jna 79 | jna-platform 80 | ${jna.version} 81 | 82 | 83 | 84 | 85 | commons-net 86 | commons-net 87 | ${commons-net.version} 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | io.quarkus 96 | quarkus-maven-plugin 97 | ${quarkus.version} 98 | 99 | 100 | io.smallrye 101 | jandex-maven-plugin 102 | ${jandex-maven-plugin.version} 103 | 104 | 105 | org.apache.maven.plugins 106 | maven-compiler-plugin 107 | ${compiler-plugin.version} 108 | 109 | 110 | org.codehaus.mojo 111 | jaxb2-maven-plugin 112 | ${jaxb2-maven-plugin.version} 113 | 114 | 115 | 116 | com.sun.xml.bind 117 | jaxb-xjc 118 | ${jaxb-xjc.version} 119 | 120 | 121 | org.jvnet.jaxb2_commons 122 | jaxb2-fluent-api 123 | ${jaxb2-fluent-api.version} 124 | 125 | 126 | org.jvnet.jaxb2_commons 127 | jaxb2-default-value 128 | ${jaxb2-default-value.version} 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | io.smallrye 137 | jandex-maven-plugin 138 | 139 | 140 | make-index 141 | 142 | jandex 143 | 144 | 145 | 146 | 147 | 148 | org.apache.maven.plugins 149 | maven-compiler-plugin 150 | 151 | ${maven.compiler.source} 152 | ${maven.compiler.target} 153 | true 154 | 155 | 160 | -Xlint:all,-path,-serial 161 | 162 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.tfyre.bambu 6 | bambu-parent 7 | 1.7.0 8 | 9 | bambu-server 10 | jar 11 | 12 | 13 | ${project.groupId} 14 | bambu-common 15 | ${project.version} 16 | 17 | 18 | io.quarkus 19 | quarkus-grpc 20 | 21 | 22 | 23 | 24 | io.quarkus 25 | quarkus-core 26 | 27 | 28 | io.quarkus 29 | quarkus-scheduler 30 | 31 | 32 | 33 | 34 | 35 | package quarkus:dev 36 | 37 | 38 | io.quarkus 39 | quarkus-maven-plugin 40 | ${quarkus.version} 41 | true 42 | 43 | 44 | 45 | build 46 | generate-code 47 | generate-code-tests 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /server/src/main/java/com/tfyre/bambu/server/BambuConfig.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.server; 2 | 3 | import io.smallrye.config.ConfigMapping; 4 | import io.smallrye.config.WithDefault; 5 | import java.util.Map; 6 | import java.util.Optional; 7 | 8 | /** 9 | * 10 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 11 | */ 12 | @ConfigMapping(prefix = "bambu") 13 | public interface BambuConfig { 14 | 15 | Map printers(); 16 | 17 | public interface Printer { 18 | 19 | @WithDefault("true") 20 | boolean enabled(); 21 | 22 | String deviceId(); 23 | 24 | String url(); 25 | 26 | @WithDefault("bblp") 27 | String username(); 28 | 29 | String accessCode(); 30 | 31 | Optional reportTopic(); 32 | 33 | Optional requestTopic(); 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /server/src/main/java/com/tfyre/bambu/server/BambuPrinterProcessor.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.server; 2 | 3 | import com.google.protobuf.InvalidProtocolBufferException; 4 | import com.google.protobuf.util.JsonFormat; 5 | import com.tfyre.bambu.model.BambuMessage; 6 | import io.quarkus.logging.Log; 7 | import io.quarkus.scheduler.Scheduler; 8 | import java.io.BufferedReader; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.InputStreamReader; 12 | import java.security.SecureRandom; 13 | import java.util.Map; 14 | import java.util.Optional; 15 | import java.util.Random; 16 | import java.util.concurrent.ConcurrentHashMap; 17 | import java.util.concurrent.atomic.AtomicInteger; 18 | import java.util.concurrent.atomic.AtomicLong; 19 | import java.util.stream.Collectors; 20 | import org.apache.camel.CamelContext; 21 | import org.apache.camel.Endpoint; 22 | import org.apache.camel.Exchange; 23 | import org.apache.camel.Message; 24 | import org.apache.camel.Processor; 25 | import org.apache.camel.ProducerTemplate; 26 | import org.jboss.logging.Logger; 27 | 28 | /** 29 | * 30 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 31 | */ 32 | public class BambuPrinterProcessor implements Processor { 33 | 34 | private static final JsonFormat.Printer PRINTER = JsonFormat.printer().preservingProtoFieldNames(); 35 | private static final JsonFormat.Parser PARSER = JsonFormat.parser().ignoringUnknownFields(); 36 | private static final Map MAP = new ConcurrentHashMap<>(); 37 | private static final Random RND = new SecureRandom(); 38 | private static final String RES_STATUS = "status"; 39 | private static final String RES_FULLSTATUS = "fullstatus"; 40 | 41 | private final AtomicLong counter = new AtomicLong(Math.abs(RND.nextInt())); 42 | private final AtomicInteger time = new AtomicInteger(RND.nextInt(100)); 43 | private final Endpoint endpoint; 44 | private ProducerTemplate producerTemplate; 45 | private final String name; 46 | 47 | public BambuPrinterProcessor(final Endpoint endpoint, final String name) { 48 | this.endpoint = endpoint; 49 | this.name = name; 50 | } 51 | 52 | @Override 53 | public void process(final Exchange exchange) throws Exception { 54 | final Message message = exchange.getMessage(); 55 | final String body = message.getBody(String.class); 56 | Log.debugf("%s: Received - [%d]", name, body.length()); 57 | Log.tracef("%s: Received RAW: %s", name, body); 58 | if (body.contains("pushall")) { 59 | sendFullStatus(); 60 | } 61 | } 62 | 63 | private BambuMessage.Builder fromJson(final String data) { 64 | final BambuMessage.Builder builder = BambuMessage.newBuilder(); 65 | try { 66 | PARSER.merge(data, builder); 67 | } catch (InvalidProtocolBufferException ex) { 68 | Log.errorf(ex, "Cannot build message: %s", ex.getMessage()); 69 | } 70 | return builder; 71 | } 72 | 73 | private String getDataFromResource(final String name) { 74 | final String fullName = String.format("json/%s.json", name); 75 | try { 76 | try (final InputStream resource = Thread.currentThread().getContextClassLoader().getResourceAsStream(fullName)) { 77 | if (resource == null) { 78 | return null; 79 | } 80 | try (InputStreamReader isr = new InputStreamReader(resource); BufferedReader reader = new BufferedReader(isr)) { 81 | return reader.lines().collect(Collectors.joining(System.lineSeparator())); 82 | } 83 | } 84 | } catch (IOException ex) { 85 | throw new IllegalStateException(String.format("Cannot read %s - %s", fullName, ex.getMessage())); 86 | } 87 | } 88 | 89 | private Optional fromMap(final String name) { 90 | return Optional.ofNullable(MAP.computeIfAbsent(name, this::getDataFromResource)); 91 | } 92 | 93 | private BambuMessage.Builder fromResource(final String name) { 94 | final String data = fromMap("%s-%s".formatted(name, this.name)) 95 | .or(() -> fromMap(name)) 96 | .get(); 97 | return fromJson(data); 98 | } 99 | 100 | private Optional toJson(final BambuMessage.Builder builder) { 101 | try { 102 | return Optional.of(PRINTER.print(builder)); 103 | } catch (InvalidProtocolBufferException ex) { 104 | Log.errorf(ex, "Cannot build message: %s", ex.getMessage()); 105 | return Optional.empty(); 106 | } 107 | } 108 | 109 | private void sendData(final String data) { 110 | if (producerTemplate == null) { 111 | return; 112 | } 113 | Log.debugf("%s: Sending - [%d]", name, data.length()); 114 | Log.tracef("%s: Sending RAW: %s", name, data); 115 | producerTemplate.sendBody(endpoint, data); 116 | } 117 | 118 | public void sendStatus() { 119 | if (time.decrementAndGet() < 1) { 120 | time.set(100); 121 | } 122 | 123 | final BambuMessage.Builder builder = fromResource(RES_STATUS); 124 | builder.getPrintBuilder() 125 | .setSequenceId("%d".formatted(counter.incrementAndGet())) 126 | .setNozzleTemper(RND.nextDouble(230)) 127 | .setBedTemper(RND.nextDouble(65)) 128 | .setMcRemainingTime(time.get()); 129 | toJson(builder) 130 | .ifPresent(this::sendData); 131 | } 132 | 133 | public void start(final CamelContext context, final Scheduler scheduler) { 134 | Log.debug("start"); 135 | producerTemplate = context.createProducerTemplate(); 136 | scheduler.newJob("%s#%s".formatted(getClass().getSimpleName(), this.name)) 137 | .setInterval("1s") 138 | .setTask(c -> sendStatus()) 139 | .schedule(); 140 | } 141 | 142 | private void sendFullStatus() { 143 | final BambuMessage.Builder builder = fromResource(RES_FULLSTATUS); 144 | builder.getPrintBuilder() 145 | .setSequenceId("%d".formatted(counter.incrementAndGet())) 146 | .setSpdLvl(RND.nextInt(4) + 1) 147 | .setChamberTemper(RND.nextDouble(55)); 148 | builder.getPrintBuilder().getLightsReportBuilder(0).setMode(counter.get() % 20 >= 10 ? "on" : "off"); 149 | toJson(builder) 150 | .ifPresent(this::sendData); 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /server/src/main/java/com/tfyre/bambu/server/CamelController.java: -------------------------------------------------------------------------------- 1 | package com.tfyre.bambu.server; 2 | 3 | import com.tfyre.bambu.mqtt.AbstractMqttController; 4 | import com.tfyre.bambu.server.BambuConfig.Printer; 5 | import io.quarkus.logging.Log; 6 | import io.quarkus.runtime.Startup; 7 | import io.quarkus.scheduler.Scheduler; 8 | import jakarta.enterprise.context.ApplicationScoped; 9 | import jakarta.inject.Inject; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import org.apache.camel.CamelContext; 13 | import org.apache.camel.Endpoint; 14 | import org.apache.camel.StartupListener; 15 | 16 | /** 17 | * 18 | * @author Francois Steyn - (fsteyn@tfyre.co.za) 19 | */ 20 | @Startup 21 | @ApplicationScoped 22 | public class CamelController extends AbstractMqttController implements StartupListener { 23 | 24 | @Inject 25 | BambuConfig config; 26 | @Inject 27 | Scheduler scheduler; 28 | 29 | private final List list = new ArrayList<>(); 30 | 31 | @Override 32 | public void onCamelContextStarted(final CamelContext context, final boolean alreadyStarted) throws Exception { 33 | 34 | } 35 | 36 | @Override 37 | public void onCamelContextFullyStarted(final CamelContext context, final boolean alreadyStarted) throws Exception { 38 | Log.info("Starting all printers"); 39 | list.forEach(p -> p.start(context, scheduler)); 40 | } 41 | 42 | @Override 43 | public void configure() throws Exception { 44 | getCamelContext().addStartupListener(this); 45 | config.printers().forEach(this::configurePrinter); 46 | Log.info("configured"); 47 | } 48 | 49 | private BambuPrinterProcessor newPrinter(final Endpoint endpoint, final String name) { 50 | final BambuPrinterProcessor printer = new BambuPrinterProcessor(endpoint, name); 51 | list.add(printer); 52 | return printer; 53 | } 54 | 55 | private void configurePrinter(final String name, final Printer config) { 56 | if (!config.enabled()) { 57 | Log.infof("Skipping: %s", name); 58 | return; 59 | } 60 | Log.infof("Configuring: %s", name); 61 | 62 | final Endpoint ep = getPrinterEndpoint(name); 63 | final BambuPrinterProcessor printer = newPrinter(ep, name); 64 | 65 | //producer 66 | from(ep) 67 | .id("producer-%s".formatted(name)) 68 | .group(name) 69 | .to(getMqttEndpoint(getTopic(config.reportTopic(), config.deviceId(), "report"), config.url(), config.username(), config.accessCode())); 70 | //consumer 71 | from(getMqttEndpoint(getTopic(config.requestTopic(), config.deviceId(), "request"), config.url(), config.username(), config.accessCode())) 72 | .id("consumer-%s".formatted(name)) 73 | .group(name) 74 | .process(printer); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /server/src/main/resources/META-INF/beans.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /server/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | quarkus.scheduler.start-mode=forced 2 | -------------------------------------------------------------------------------- /server/src/main/resources/json/status.json: -------------------------------------------------------------------------------- 1 | { 2 | "print": { 3 | "nozzle_temper": 229.9375, 4 | "bed_temper": 64.96875, 5 | "mc_remaining_time": 100, 6 | "mc_print_line_number": "280515", 7 | "command": "push_status", 8 | "msg": 1, 9 | "sequence_id": "FIXME" 10 | } 11 | } 12 | 13 | --------------------------------------------------------------------------------