├── .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, ENTITY> 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 |
--------------------------------------------------------------------------------