Compare commits
56 Commits
Author | SHA1 | Date | |
---|---|---|---|
383f0c3cb3 | |||
8950778146 | |||
52ba3ff214 | |||
98ab94b371 | |||
b54e372342 | |||
76f706df19 | |||
5c8a9787d4 | |||
e3fda73dd3 | |||
5633d6f6ea | |||
884101c0ba | |||
7c87ba9482 | |||
1d9f874560 | |||
21e30034d0 | |||
3c8b904a26 | |||
55ad4c1718 | |||
4ef8e40702 | |||
2537bc8f57 | |||
28be048300 | |||
eaa0ed94c0 | |||
14af93b1d0 | |||
d94df9dd85 | |||
d0fd4a5434 | |||
3c218a548d | |||
03af51608d | |||
c00285b1b2 | |||
7f1ae5a24b | |||
5754e81b72 | |||
cd4103cc71 | |||
01c6cacf15 | |||
fda4b86cbc | |||
ad862d5ebd | |||
b04202463d | |||
8e3eb9c64b | |||
e1ea88dbae | |||
ac7bb16a2b | |||
112210a3f1 | |||
176154cfee | |||
56e5fb4dd2 | |||
3e7708429d | |||
908173214f | |||
df4bd6cf44 | |||
bcb1e0f649 | |||
f37eadf3ea | |||
48027f6008 | |||
616fdf7a82 | |||
c110fa140a | |||
3889efa67a | |||
cb4b345aa7 | |||
3d0cc26147 | |||
ff58636e41 | |||
f54ab5c29f | |||
7a4439c53e | |||
9eb8b0da65 | |||
ef969d3d3b | |||
3d92d73879 | |||
41dad23c49 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
github: [jneilliii]
|
||||
patreon: jneilliii
|
||||
custom: ['https://www.paypal.me/jneilliii']
|
26
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Please make sure to check other issues, including closed ones, prior to submitting a bug report. Debug logs are required and any bug report submitted without them will be ignored and closed.
|
||||
title: "[BUG]: "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the Bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**Expected Behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Debug Logs**
|
||||
<!-- If logs are not included in your bug report it will be closed. Enable debug logging for octoprint.plugins.bambu_printer in OctoPrint's logging section of settings and recreate the issue then attach octoprint.log and plugin_bambu_printer_serial.log to this bug report. -->
|
||||
|
||||
**Screenshots**
|
||||
<!-- Please share any relevant screenshots related to the issue. -->
|
||||
|
||||
**Printer and Plugin Setting Details**
|
||||
|
||||
* Printer model?
|
||||
* Is your printer connected to Bambu Cloud?
|
||||
* Is the plugin configured for local access only?
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Create a feature request for an improvement or change you'd like implemented.
|
||||
title: "[FR]: "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
16
.github/stale.yml
vendored
Normal file
16
.github/stale.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 14
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- enhancement
|
||||
- bug
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
activity in 14 days. It will be closed if no further activity occurs in 7 days.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
21
.github/workflows/issue-validator.yml
vendored
Normal file
21
.github/workflows/issue-validator.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
name: issue validator
|
||||
on:
|
||||
workflow_dispatch:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Okabe-Junya/issue-validator@v0.4.1
|
||||
with:
|
||||
body: '/\[(octoprint\.log)\]|\[(plugin_bambu_printer_serial\.log)\]/g'
|
||||
body-regex-flags: 'true'
|
||||
is-auto-close: 'true'
|
||||
issue-type: 'both'
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
27
.github/workflows/stale.yml
vendored
Normal file
27
.github/workflows/stale.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
name: Mark Stale Issues
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
permissions:
|
||||
actions: write
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue has been automatically marked as stale because it has not had activity in 14 days. It will be closed if no further activity occurs in 7 days'
|
||||
days-before-stale: 14
|
||||
days-before-close: 7
|
||||
stale-issue-label: 'stale'
|
||||
days-before-issue-stale: 14
|
||||
days-before-pr-stale: -1
|
||||
days-before-issue-close: 7
|
||||
days-before-pr-close: -1
|
||||
exempt-issue-labels: 'bug,enhancement'
|
||||
- uses: actions/checkout@v4
|
||||
- uses: gautamkrishnar/keepalive-workflow@v2
|
||||
with:
|
||||
use_api: true
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -8,3 +8,5 @@ dist
|
||||
.DS_Store
|
||||
*.zip
|
||||
extras
|
||||
|
||||
test/test_output
|
661
LICENSE
Normal file
661
LICENSE
Normal file
@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
@ -2,3 +2,4 @@ include README.md
|
||||
recursive-include octoprint_bambu_printer/templates *
|
||||
recursive-include octoprint_bambu_printer/translations *
|
||||
recursive-include octoprint_bambu_printer/static *
|
||||
include octoprint_bambu_printer/printer/pybambu/filaments.json
|
||||
|
16
README.md
16
README.md
@ -1,17 +1,13 @@
|
||||
# OctoPrint-BambuPrinter
|
||||
|
||||
**TODO:** Describe what your plugin does.
|
||||
This plugin is an attempt to connect BambuLab printers to OctoPrint. It's still a work in progress, and there may be bugs/quirks that you will have to work around while using the plugin and during development.
|
||||
|
||||
## System Requirements
|
||||
|
||||
* Python 3.9 or higher (OctoPi 1.0.0)
|
||||
|
||||
## Setup
|
||||
|
||||
Install via the bundled [Plugin Manager](https://docs.octoprint.org/en/master/bundledplugins/pluginmanager.html)
|
||||
or manually using this URL:
|
||||
Install manually using this URL:
|
||||
|
||||
https://github.com/jneilliii/OctoPrint-BambuPrinter/archive/master.zip
|
||||
|
||||
**TODO:** Describe how to install your plugin, if more needs to be done than just installing it via pip or through
|
||||
the plugin manager.
|
||||
|
||||
## Configuration
|
||||
|
||||
**TODO:** Describe your plugin's configuration options (if any).
|
||||
|
@ -1,127 +1,10 @@
|
||||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
from .ftpsclient import IoTFTPSClient
|
||||
|
||||
|
||||
class BambuPrintPlugin(
|
||||
octoprint.plugin.SettingsPlugin, octoprint.plugin.TemplatePlugin
|
||||
):
|
||||
|
||||
def get_template_configs(self):
|
||||
return [{"type": "settings", "custom_bindings": False}]
|
||||
|
||||
def get_settings_defaults(self):
|
||||
return {"device_type": "X1C",
|
||||
"serial": "",
|
||||
"host": "",
|
||||
"access_code": "",
|
||||
"username": "bblp",
|
||||
"timelapse": False,
|
||||
"bed_leveling": True,
|
||||
"flow_cali": False,
|
||||
"vibration_cali": True,
|
||||
"layer_inspect": True,
|
||||
"use_ams": False,
|
||||
"local_mqtt": True,
|
||||
"region": "",
|
||||
"email": "",
|
||||
"auth_token": ""}
|
||||
|
||||
def support_3mf_files(self):
|
||||
return {'machinecode': {'3mf': ["3mf"]}}
|
||||
|
||||
def upload_to_sd(self, printer, filename, path, sd_upload_started, sd_upload_succeeded, sd_upload_failed, *args, **kwargs):
|
||||
self._logger.debug(f"Starting upload from {filename} to {filename}")
|
||||
sd_upload_started(filename, filename)
|
||||
def process():
|
||||
host = self._settings.get(["host"])
|
||||
access_code = self._settings.get(["access_code"])
|
||||
elapsed = time.monotonic()
|
||||
|
||||
try:
|
||||
ftp = IoTFTPSClient(f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True)
|
||||
if ftp.upload_file(path, f"{filename}"):
|
||||
elapsed = time.monotonic() - elapsed
|
||||
sd_upload_succeeded(filename, filename, elapsed)
|
||||
# remove local file after successful upload to Bambu
|
||||
self._file_manager.remove_file("local", filename)
|
||||
else:
|
||||
raise Exception("upload failed")
|
||||
except Exception as e:
|
||||
elapsed = time.monotonic() - elapsed
|
||||
sd_upload_failed(filename, filename, elapsed)
|
||||
self._logger.debug(f"Error uploading file {filename}")
|
||||
|
||||
thread = threading.Thread(target=process)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
return filename
|
||||
|
||||
def virtual_printer_factory(self, comm_instance, port, baudrate, read_timeout):
|
||||
if not port == "BAMBU":
|
||||
return None
|
||||
|
||||
if self._settings.get(["serial"]) == "" or self._settings.get(["host"]) == "" or self._settings.get(["access_code"]) == "":
|
||||
return None
|
||||
|
||||
import logging.handlers
|
||||
|
||||
from octoprint.logging.handlers import CleaningTimedRotatingFileHandler
|
||||
|
||||
seriallog_handler = CleaningTimedRotatingFileHandler(
|
||||
self._settings.get_plugin_logfile_path(postfix="serial"),
|
||||
when="D",
|
||||
backupCount=3,
|
||||
)
|
||||
seriallog_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
|
||||
seriallog_handler.setLevel(logging.DEBUG)
|
||||
|
||||
from . import virtual
|
||||
|
||||
serial_obj = virtual.BambuPrinter(
|
||||
self._settings,
|
||||
self._printer_profile_manager,
|
||||
data_folder=self.get_plugin_data_folder(),
|
||||
seriallog_handler=seriallog_handler,
|
||||
read_timeout=float(read_timeout),
|
||||
faked_baudrate=baudrate,
|
||||
)
|
||||
return serial_obj
|
||||
|
||||
def get_additional_port_names(self, *args, **kwargs):
|
||||
if self._settings.get(["serial"]) != "" and self._settings.get(["host"]) != "" and self._settings.get(["access_code"]) != "":
|
||||
return ["BAMBU"]
|
||||
else:
|
||||
return []
|
||||
|
||||
def get_update_information(self):
|
||||
return {'bambu_printer': {'displayName': "Bambu Printer",
|
||||
'displayVersion': self._plugin_version,
|
||||
'type': "github_release",
|
||||
'user': "jneilliii",
|
||||
'repo': "OctoPrint-BambuPrinter",
|
||||
'current': self._plugin_version,
|
||||
'stable_branch': {'name': "Stable",
|
||||
'branch': "master",
|
||||
'comittish': ["master"]},
|
||||
'prerelease_branches': [
|
||||
{'name': "Release Candidate",
|
||||
'branch': "rc",
|
||||
'comittish': ["rc", "master"]}
|
||||
],
|
||||
'pip': "https://github.com/jneilliii/OctoPrint-BambuPrinter/archive/{target_version}.zip"}}
|
||||
|
||||
|
||||
__plugin_name__ = "Bambu Printer"
|
||||
__plugin_pythoncompat__ = ">=3.7,<4"
|
||||
|
||||
from .bambu_print_plugin import BambuPrintPlugin
|
||||
|
||||
|
||||
def __plugin_load__():
|
||||
plugin = BambuPrintPlugin()
|
||||
@ -136,4 +19,6 @@ def __plugin_load__():
|
||||
"octoprint.filemanager.extension_tree": __plugin_implementation__.support_3mf_files,
|
||||
"octoprint.printer.sdcardupload": __plugin_implementation__.upload_to_sd,
|
||||
"octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information,
|
||||
"octoprint.server.api.before_request": __plugin_implementation__._hook_octoprint_server_api_before_request,
|
||||
"octoprint.server.http.routes": __plugin_implementation__.route_hook,
|
||||
}
|
||||
|
360
octoprint_bambu_printer/bambu_print_plugin.py
Normal file
360
octoprint_bambu_printer/bambu_print_plugin.py
Normal file
@ -0,0 +1,360 @@
|
||||
from __future__ import absolute_import, annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import threading
|
||||
from time import perf_counter
|
||||
from contextlib import contextmanager
|
||||
import flask
|
||||
import logging.handlers
|
||||
from urllib.parse import quote as urlquote
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
import octoprint.printer
|
||||
import octoprint.server
|
||||
import octoprint.plugin
|
||||
from octoprint.events import Events
|
||||
import octoprint.settings
|
||||
from octoprint.util import is_hidden_path
|
||||
from octoprint.server.util.flask import no_firstrun_access
|
||||
from octoprint.server.util.tornado import (
|
||||
LargeResponseHandler,
|
||||
path_validation_factory,
|
||||
)
|
||||
from octoprint.access.permissions import Permissions
|
||||
from octoprint.logging.handlers import CleaningTimedRotatingFileHandler
|
||||
|
||||
from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView
|
||||
from octoprint_bambu_printer.printer.pybambu import BambuCloud
|
||||
|
||||
from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import (
|
||||
RemoteSDCardFileList,
|
||||
)
|
||||
|
||||
from .printer.file_system.bambu_timelapse_file_info import (
|
||||
BambuTimelapseFileInfo,
|
||||
)
|
||||
from .printer.bambu_virtual_printer import BambuVirtualPrinter
|
||||
|
||||
|
||||
@contextmanager
|
||||
def measure_elapsed():
|
||||
start = perf_counter()
|
||||
|
||||
def _get_elapsed():
|
||||
return perf_counter() - start
|
||||
|
||||
yield _get_elapsed
|
||||
print(f"Total elapsed: {_get_elapsed()}")
|
||||
|
||||
|
||||
class BambuPrintPlugin(
|
||||
octoprint.plugin.SettingsPlugin,
|
||||
octoprint.plugin.TemplatePlugin,
|
||||
octoprint.plugin.AssetPlugin,
|
||||
octoprint.plugin.EventHandlerPlugin,
|
||||
octoprint.plugin.SimpleApiPlugin,
|
||||
octoprint.plugin.BlueprintPlugin,
|
||||
):
|
||||
_logger: logging.Logger
|
||||
_plugin_manager: octoprint.plugin.PluginManager
|
||||
_bambu_file_system: RemoteSDCardFileList
|
||||
_timelapse_files_view: CachedFileView
|
||||
_bambu_cloud: None
|
||||
|
||||
def on_settings_initialized(self):
|
||||
self._bambu_file_system = RemoteSDCardFileList(self._settings)
|
||||
self._timelapse_files_view = CachedFileView(self._bambu_file_system)
|
||||
if self._settings.get(["device_type"]) in ["X1", "X1C"]:
|
||||
self._timelapse_files_view.with_filter("timelapse/", ".mp4")
|
||||
else:
|
||||
self._timelapse_files_view.with_filter("timelapse/", ".avi")
|
||||
|
||||
def get_assets(self):
|
||||
return {"js": ["js/jquery-ui.min.js", "js/knockout-sortable.1.2.0.js", "js/bambu_printer.js"],
|
||||
"css": ["css/bambu_printer.css"]
|
||||
}
|
||||
|
||||
def get_template_configs(self):
|
||||
return [
|
||||
{"type": "settings", "custom_bindings": True},
|
||||
{
|
||||
"type": "generic",
|
||||
"custom_bindings": True,
|
||||
"template": "bambu_timelapse.jinja2",
|
||||
},
|
||||
{"type": "generic", "custom_bindings": True, "template": "bambu_printer.jinja2"}]
|
||||
|
||||
def get_settings_defaults(self):
|
||||
return {
|
||||
"device_type": "X1C",
|
||||
"serial": "",
|
||||
"host": "",
|
||||
"access_code": "",
|
||||
"username": "bblp",
|
||||
"timelapse": False,
|
||||
"bed_leveling": True,
|
||||
"flow_cali": False,
|
||||
"vibration_cali": True,
|
||||
"layer_inspect": False,
|
||||
"use_ams": False,
|
||||
"local_mqtt": True,
|
||||
"region": "",
|
||||
"email": "",
|
||||
"auth_token": "",
|
||||
"always_use_default_options": False,
|
||||
"ams_data": [],
|
||||
"ams_mapping": [],
|
||||
"ams_current_tray": 255,
|
||||
}
|
||||
|
||||
def on_settings_save(self, data):
|
||||
if data.get("local_mqtt", False) is True:
|
||||
data["auth_token"] = ""
|
||||
octoprint.plugin.SettingsPlugin.on_settings_save(self, data)
|
||||
|
||||
def is_api_adminonly(self):
|
||||
return True
|
||||
|
||||
def get_api_commands(self):
|
||||
return {"register": ["email", "password", "region", "auth_token"],
|
||||
"verify": ["auth_type", "password"]}
|
||||
|
||||
def on_api_command(self, command, data):
|
||||
if command == "register":
|
||||
if (
|
||||
"email" in data
|
||||
and "password" in data
|
||||
and "region" in data
|
||||
and "auth_token" in data
|
||||
):
|
||||
self._logger.info(f"Registering user {data['email']}")
|
||||
self._bambu_cloud = BambuCloud(data["region"], data["email"], data["password"], data["auth_token"])
|
||||
auth_response = self._bambu_cloud.login(data["region"], data["email"], data["password"])
|
||||
return flask.jsonify(
|
||||
{
|
||||
"auth_response": auth_response,
|
||||
}
|
||||
)
|
||||
elif command == "verify":
|
||||
auth_response = None
|
||||
if (
|
||||
"auth_type" in data
|
||||
and "password" in data
|
||||
and self._bambu_cloud is not None
|
||||
):
|
||||
self._logger.info(f"Verifying user {self._bambu_cloud._email}")
|
||||
if data["auth_type"] == "verifyCode":
|
||||
auth_response = self._bambu_cloud.login_with_verification_code(data["password"])
|
||||
elif data["auth_type"] == "tfa":
|
||||
auth_response = self._bambu_cloud.login_with_2fa_code(data["password"])
|
||||
else:
|
||||
self._logger.warning(f"Unknown verification type: {data['auth_type']}")
|
||||
|
||||
if auth_response == "success":
|
||||
return flask.jsonify(
|
||||
{
|
||||
"auth_token": self._bambu_cloud.auth_token,
|
||||
"username": self._bambu_cloud.username
|
||||
}
|
||||
)
|
||||
else:
|
||||
self._logger.info(f"Error verifying: {auth_response}")
|
||||
return flask.jsonify(
|
||||
{
|
||||
"error": "Unable to verify"
|
||||
}
|
||||
)
|
||||
|
||||
def on_event(self, event, payload):
|
||||
if event == Events.TRANSFER_DONE:
|
||||
self._printer.commands("M20 L T", force=True)
|
||||
elif event == Events.FILE_ADDED:
|
||||
if payload["operation"] == "add" and "3mf" in payload["type"]:
|
||||
file_container = os.path.join(self._settings.getBaseFolder("uploads"), payload["path"])
|
||||
with zipfile.ZipFile(file_container) as z:
|
||||
with z.open("Metadata/plate_1.json", "r") as json_data:
|
||||
plate_data = json.load(json_data)
|
||||
|
||||
if plate_data:
|
||||
self._file_manager.set_additional_metadata("sdcard", payload["path"], "plate_data", plate_data, overwrite=True)
|
||||
|
||||
def support_3mf_files(self):
|
||||
return {"machinecode": {"3mf": ["3mf"]}}
|
||||
|
||||
def upload_to_sd(
|
||||
self,
|
||||
printer,
|
||||
filename,
|
||||
path,
|
||||
sd_upload_started,
|
||||
sd_upload_succeeded,
|
||||
sd_upload_failed,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self._logger.debug(f"Starting upload from {filename} to {filename}")
|
||||
sd_upload_started(filename, filename)
|
||||
|
||||
def process():
|
||||
with measure_elapsed() as get_elapsed:
|
||||
try:
|
||||
with self._bambu_file_system.get_ftps_client() as ftp:
|
||||
if ftp.upload_file(path, f"{filename}"):
|
||||
sd_upload_succeeded(filename, filename, get_elapsed())
|
||||
else:
|
||||
raise Exception("upload failed")
|
||||
except Exception as e:
|
||||
sd_upload_failed(filename, filename, get_elapsed())
|
||||
self._logger.exception(e)
|
||||
|
||||
thread = threading.Thread(target=process)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
return filename
|
||||
|
||||
def get_template_vars(self):
|
||||
return {"plugin_version": self._plugin_version}
|
||||
|
||||
def virtual_printer_factory(self, comm_instance, port, baudrate, read_timeout):
|
||||
if not port == "BAMBU":
|
||||
return None
|
||||
if (
|
||||
self._settings.get(["serial"]) == ""
|
||||
or self._settings.get(["host"]) == ""
|
||||
or self._settings.get(["access_code"]) == ""
|
||||
):
|
||||
return None
|
||||
seriallog_handler = CleaningTimedRotatingFileHandler(
|
||||
self._settings.get_plugin_logfile_path(postfix="serial"),
|
||||
when="D",
|
||||
backupCount=3,
|
||||
)
|
||||
seriallog_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
|
||||
seriallog_handler.setLevel(logging.DEBUG)
|
||||
|
||||
serial_obj = BambuVirtualPrinter(
|
||||
self._settings,
|
||||
self._printer_profile_manager,
|
||||
data_folder=self.get_plugin_data_folder(),
|
||||
serial_log_handler=seriallog_handler,
|
||||
read_timeout=float(read_timeout),
|
||||
faked_baudrate=baudrate,
|
||||
)
|
||||
return serial_obj
|
||||
|
||||
def get_additional_port_names(self, *args, **kwargs):
|
||||
if (
|
||||
self._settings.get(["serial"]) != ""
|
||||
and self._settings.get(["host"]) != ""
|
||||
and self._settings.get(["access_code"]) != ""
|
||||
):
|
||||
return ["BAMBU"]
|
||||
else:
|
||||
return []
|
||||
|
||||
def get_timelapse_file_list(self):
|
||||
if flask.request.path.startswith("/api/timelapse"):
|
||||
|
||||
def process():
|
||||
return_file_list = []
|
||||
for file_info in self._timelapse_files_view.get_all_info():
|
||||
timelapse_info = BambuTimelapseFileInfo.from_file_info(file_info)
|
||||
return_file_list.append(timelapse_info.to_dict())
|
||||
self._plugin_manager.send_plugin_message(
|
||||
self._identifier, {"files": return_file_list}
|
||||
)
|
||||
|
||||
thread = threading.Thread(target=process)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def _hook_octoprint_server_api_before_request(self, *args, **kwargs):
|
||||
return [self.get_timelapse_file_list]
|
||||
|
||||
def _download_file(self, file_name: str, source_path: str):
|
||||
destination = Path(self.get_plugin_data_folder()) / file_name
|
||||
if destination.exists():
|
||||
return destination
|
||||
|
||||
with self._bambu_file_system.get_ftps_client() as ftp:
|
||||
ftp.download_file(
|
||||
source=(Path(source_path) / file_name).as_posix(),
|
||||
dest=destination.as_posix(),
|
||||
)
|
||||
return destination
|
||||
|
||||
@octoprint.plugin.BlueprintPlugin.route("/timelapse/<filename>", methods=["GET"])
|
||||
@octoprint.server.util.flask.restricted_access
|
||||
@no_firstrun_access
|
||||
@Permissions.TIMELAPSE_DOWNLOAD.require(403)
|
||||
def downloadTimelapse(self, filename):
|
||||
self._download_file(filename, "timelapse/")
|
||||
return flask.redirect(
|
||||
"/plugin/bambu_printer/download/timelapse/" + urlquote(filename), code=302
|
||||
)
|
||||
|
||||
@octoprint.plugin.BlueprintPlugin.route("/thumbnail/<filename>", methods=["GET"])
|
||||
@octoprint.server.util.flask.restricted_access
|
||||
@no_firstrun_access
|
||||
@Permissions.TIMELAPSE_DOWNLOAD.require(403)
|
||||
def downloadThumbnail(self, filename):
|
||||
self._download_file(filename, "timelapse/thumbnail/")
|
||||
return flask.redirect(
|
||||
"/plugin/bambu_printer/download/thumbnail/" + urlquote(filename), code=302
|
||||
)
|
||||
|
||||
def is_blueprint_csrf_protected(self):
|
||||
return True
|
||||
|
||||
def route_hook(self, server_routes, *args, **kwargs):
|
||||
return [
|
||||
(
|
||||
r"/download/timelapse/(.*)",
|
||||
LargeResponseHandler,
|
||||
{
|
||||
"path": self.get_plugin_data_folder(),
|
||||
"as_attachment": True,
|
||||
"path_validation": path_validation_factory(
|
||||
lambda path: not is_hidden_path(path), status_code=404
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
r"/download/thumbnail/(.*)",
|
||||
LargeResponseHandler,
|
||||
{
|
||||
"path": self.get_plugin_data_folder(),
|
||||
"as_attachment": True,
|
||||
"path_validation": path_validation_factory(
|
||||
lambda path: not is_hidden_path(path), status_code=404
|
||||
),
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
def get_update_information(self):
|
||||
return {
|
||||
"bambu_printer": {
|
||||
"displayName": "Bambu Printer",
|
||||
"displayVersion": self._plugin_version,
|
||||
"type": "github_release",
|
||||
"user": "jneilliii",
|
||||
"repo": "OctoPrint-BambuPrinter",
|
||||
"current": self._plugin_version,
|
||||
"stable_branch": {
|
||||
"name": "Stable",
|
||||
"branch": "master",
|
||||
"comittish": ["master"],
|
||||
},
|
||||
"prerelease_branches": [
|
||||
{
|
||||
"name": "Release Candidate",
|
||||
"branch": "rc",
|
||||
"comittish": ["rc", "master"],
|
||||
}
|
||||
],
|
||||
"pip": "https://github.com/jneilliii/OctoPrint-BambuPrinter/archive/{target_version}.zip",
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
from .ftpsclient import IoTFTPSClient
|
@ -1,221 +0,0 @@
|
||||
"""
|
||||
Based on: <https://github.com/dgonzo27/py-iot-utils>
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
wrapper for FTPS server interactions
|
||||
"""
|
||||
|
||||
import ftplib
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
from typing import Optional, Union, List
|
||||
|
||||
from contextlib import redirect_stdout
|
||||
import io
|
||||
import re
|
||||
|
||||
class ImplicitTLS(ftplib.FTP_TLS):
|
||||
"""ftplib.FTP_TLS sub-class to support implicit SSL FTPS"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._sock = None
|
||||
|
||||
@property
|
||||
def sock(self):
|
||||
"""return socket"""
|
||||
return self._sock
|
||||
|
||||
@sock.setter
|
||||
def sock(self, value):
|
||||
"""wrap and set SSL socket"""
|
||||
if value is not None and not isinstance(value, ssl.SSLSocket):
|
||||
value = self.context.wrap_socket(value)
|
||||
self._sock = value
|
||||
|
||||
def ntransfercmd(self, cmd, rest=None):
|
||||
conn, size = ftplib.FTP.ntransfercmd(self, cmd, rest)
|
||||
|
||||
if self._prot_p:
|
||||
conn = self.context.wrap_socket(conn,
|
||||
server_hostname=self.host,
|
||||
session=self.sock.session) # this is the fix
|
||||
return conn, size
|
||||
|
||||
|
||||
class IoTFTPSClient:
|
||||
"""iot ftps ftpsclient"""
|
||||
|
||||
ftps_host: str
|
||||
ftps_port: int
|
||||
ftps_user: str
|
||||
ftps_pass: str
|
||||
ssl_implicit: bool
|
||||
ftps_session: Union[ftplib.FTP, ImplicitTLS]
|
||||
last_error: Optional[str] = None
|
||||
welcome: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ftps_host: str,
|
||||
ftps_port: Optional[int] = 21,
|
||||
ftps_user: Optional[str] = "",
|
||||
ftps_pass: Optional[str] = "",
|
||||
ssl_implicit: Optional[bool] = False,
|
||||
) -> None:
|
||||
self.ftps_host = ftps_host
|
||||
self.ftps_port = ftps_port
|
||||
self.ftps_user = ftps_user
|
||||
self.ftps_pass = ftps_pass
|
||||
self.ssl_implicit = ssl_implicit
|
||||
self.instantiate_ftps_session()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"IoT FTPS Client\n"
|
||||
"--------------------\n"
|
||||
f"host: {self.ftps_host}\n"
|
||||
f"port: {self.ftps_port}\n"
|
||||
f"user: {self.ftps_user}\n"
|
||||
f"ssl: {self.ssl_implicit}"
|
||||
)
|
||||
|
||||
def instantiate_ftps_session(self) -> None:
|
||||
"""init ftps_session based on input params"""
|
||||
self.ftps_session = ImplicitTLS() if self.ssl_implicit else ftplib.FTP()
|
||||
self.ftps_session.set_debuglevel(0)
|
||||
|
||||
self.welcome = self.ftps_session.connect(
|
||||
host=self.ftps_host, port=self.ftps_port)
|
||||
|
||||
if self.ftps_user and self.ftps_pass:
|
||||
self.ftps_session.login(user=self.ftps_user, passwd=self.ftps_pass)
|
||||
else:
|
||||
self.ftps_session.login()
|
||||
|
||||
if self.ssl_implicit:
|
||||
self.ftps_session.prot_p()
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""disconnect the current session from the ftps server"""
|
||||
self.ftps_session.close()
|
||||
|
||||
def download_file(self, source: str, dest: str):
|
||||
"""download a file to a path on the local filesystem"""
|
||||
with open(dest, "wb") as file:
|
||||
self.ftps_session.retrbinary(f"RETR {source}", file.write)
|
||||
|
||||
def upload_file(self, source: str, dest: str, callback=None):
|
||||
"""upload a file to a path inside the FTPS server"""
|
||||
|
||||
file_size = os.path.getsize(source)
|
||||
|
||||
block_size = max(file_size // 100, 8192)
|
||||
rest = None
|
||||
|
||||
# Taken from ftplib.storbinary but with custom ssl handling
|
||||
# due to the shitty bambu p1p ftps server TODO fix properly.
|
||||
with open(source, "rb") as fp:
|
||||
self.ftps_session.voidcmd('TYPE I')
|
||||
|
||||
with self.ftps_session.transfercmd(f"STOR {dest}", rest) as conn:
|
||||
while 1:
|
||||
buf = fp.read(block_size)
|
||||
|
||||
if not buf:
|
||||
break
|
||||
|
||||
conn.sendall(buf)
|
||||
|
||||
if callback:
|
||||
callback(buf)
|
||||
|
||||
# shutdown ssl layer
|
||||
if ftplib._SSLSocket is not None and isinstance(conn, ftplib._SSLSocket):
|
||||
# Yeah this is suposed to be conn.unwrap
|
||||
# But since we operate in prot p mode
|
||||
# we can close the connection always.
|
||||
# This is cursed but it works.
|
||||
if "vsFTPd" in self.welcome:
|
||||
conn.unwrap()
|
||||
else:
|
||||
conn.shutdown(socket.SHUT_RDWR)
|
||||
|
||||
return self.ftps_session.voidresp()
|
||||
|
||||
# Old api call.
|
||||
# self.ftps_session.storbinary(
|
||||
# f"STOR {dest}", file, blocksize=block_size, callback=callback)
|
||||
|
||||
def delete_file(self, path: str):
|
||||
"""delete a file from under a path inside the FTPS server"""
|
||||
self.ftps_session.delete(path)
|
||||
|
||||
def move_file(self, source: str, dest: str):
|
||||
"""move a file inside the FTPS server to another path inside the FTPS server"""
|
||||
self.ftps_session.rename(source, dest)
|
||||
|
||||
def mkdir(self, path: str) -> str:
|
||||
return self.ftps_session.mkd(path)
|
||||
|
||||
def list_files(self, path: str, file_pattern: Optional[str] = None) -> Union[List[str], None]:
|
||||
"""list files under a path inside the FTPS server"""
|
||||
try:
|
||||
files = self.ftps_session.nlst(path)
|
||||
if not files:
|
||||
return
|
||||
if file_pattern:
|
||||
return [f for f in files if file_pattern in f]
|
||||
return files
|
||||
except Exception as ex:
|
||||
print(f"unexpected exception occurred: {ex}")
|
||||
pass
|
||||
return
|
||||
|
||||
def list_files_ex(self, path: str) -> Union[list[str], None]:
|
||||
"""list files under a path inside the FTPS server"""
|
||||
try:
|
||||
f = io.StringIO()
|
||||
with redirect_stdout(f):
|
||||
self.ftps_session.dir(path)
|
||||
s = f.getvalue()
|
||||
files = []
|
||||
for row in s.split("\n"):
|
||||
if len(row) <= 0: continue
|
||||
|
||||
attribs = row.split(" ")
|
||||
|
||||
match = re.search(r".*\ (\d\d\:\d\d|\d\d\d\d)\ (.*)", row)
|
||||
name = ""
|
||||
if match:
|
||||
name = match.groups(1)[1]
|
||||
else:
|
||||
name = attribs[len(attribs) - 1]
|
||||
|
||||
file = ( attribs[0], name )
|
||||
files.append(file)
|
||||
return files
|
||||
except Exception as ex:
|
||||
print(f"unexpected exception occurred: [{ex}]")
|
||||
pass
|
||||
return
|
2
octoprint_bambu_printer/printer/__init__.py
Normal file
2
octoprint_bambu_printer/printer/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html"
|
707
octoprint_bambu_printer/printer/bambu_virtual_printer.py
Normal file
707
octoprint_bambu_printer/printer/bambu_virtual_printer.py
Normal file
@ -0,0 +1,707 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
from dataclasses import dataclass, field, asdict
|
||||
import math
|
||||
from pathlib import Path
|
||||
import queue
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView
|
||||
from octoprint_bambu_printer.printer.file_system.file_info import FileInfo
|
||||
from octoprint_bambu_printer.printer.print_job import PrintJob
|
||||
from octoprint_bambu_printer.printer.pybambu import BambuClient, commands
|
||||
import logging
|
||||
import logging.handlers
|
||||
|
||||
from octoprint.util import RepeatedTimer
|
||||
|
||||
from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState
|
||||
from octoprint_bambu_printer.printer.states.idle_state import IdleState
|
||||
|
||||
from .printer_serial_io import PrinterSerialIO
|
||||
from .states.paused_state import PausedState
|
||||
from .states.printing_state import PrintingState
|
||||
|
||||
from .gcode_executor import GCodeExecutor
|
||||
from .file_system.remote_sd_card_file_list import RemoteSDCardFileList
|
||||
|
||||
|
||||
AMBIENT_TEMPERATURE: float = 21.3
|
||||
|
||||
|
||||
@dataclass
|
||||
class BambuPrinterTelemetry:
|
||||
temp: list[float] = field(default_factory=lambda: [AMBIENT_TEMPERATURE])
|
||||
targetTemp: list[float] = field(default_factory=lambda: [0.0])
|
||||
bedTemp: float = AMBIENT_TEMPERATURE
|
||||
bedTargetTemp = 0.0
|
||||
hasChamber: bool = False
|
||||
chamberTemp: float = AMBIENT_TEMPERATURE
|
||||
chamberTargetTemp: float = 0.0
|
||||
lastTempAt: float = time.monotonic()
|
||||
firmwareName: str = "Bambu"
|
||||
extruderCount: int = 1
|
||||
ams_current_tray: int = 255
|
||||
|
||||
|
||||
# noinspection PyBroadException
|
||||
class BambuVirtualPrinter:
|
||||
gcode_executor = GCodeExecutor()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
settings,
|
||||
printer_profile_manager,
|
||||
data_folder,
|
||||
serial_log_handler=None,
|
||||
read_timeout=5.0,
|
||||
faked_baudrate=115200,
|
||||
):
|
||||
self._settings = settings
|
||||
self._printer_profile_manager = printer_profile_manager
|
||||
self._faked_baudrate = faked_baudrate
|
||||
self._data_folder = data_folder
|
||||
self._last_hms_errors = None
|
||||
self._log = logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter")
|
||||
self.ams_data = self._settings.get(["ams_data"])
|
||||
|
||||
self._state_idle = IdleState(self)
|
||||
self._state_printing = PrintingState(self)
|
||||
self._state_paused = PausedState(self)
|
||||
self._current_state = self._state_idle
|
||||
|
||||
self._running = True
|
||||
self._print_status_reporter = None
|
||||
self._print_temp_reporter = None
|
||||
self._printer_thread = threading.Thread(
|
||||
target=self._printer_worker,
|
||||
name="octoprint.plugins.bambu_printer.printer_state",
|
||||
)
|
||||
self._state_change_queue = queue.Queue()
|
||||
|
||||
self._current_print_job: PrintJob | None = None
|
||||
|
||||
self._serial_io = PrinterSerialIO(
|
||||
handle_command_callback=self._process_gcode_serial_command,
|
||||
settings=settings,
|
||||
serial_log_handler=serial_log_handler,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=10.0,
|
||||
)
|
||||
|
||||
self._telemetry = BambuPrinterTelemetry()
|
||||
self._telemetry.hasChamber = printer_profile_manager.get_current().get(
|
||||
"heatedChamber"
|
||||
)
|
||||
|
||||
self.file_system = RemoteSDCardFileList(settings)
|
||||
self._selected_project_file: FileInfo | None = None
|
||||
self._project_files_view = (
|
||||
CachedFileView(self.file_system, on_update=self._list_cached_project_files)
|
||||
.with_filter("", ".3mf")
|
||||
.with_filter("cache/", ".3mf")
|
||||
)
|
||||
|
||||
self._serial_io.start()
|
||||
self._printer_thread.start()
|
||||
|
||||
self._bambu_client: BambuClient = self._create_client_connection_async()
|
||||
|
||||
@property
|
||||
def bambu_client(self):
|
||||
return self._bambu_client
|
||||
|
||||
@property
|
||||
def is_running(self):
|
||||
return self._running
|
||||
|
||||
@property
|
||||
def current_state(self):
|
||||
return self._current_state
|
||||
|
||||
@property
|
||||
def current_print_job(self):
|
||||
return self._current_print_job
|
||||
|
||||
@current_print_job.setter
|
||||
def current_print_job(self, value):
|
||||
self._current_print_job = value
|
||||
|
||||
@property
|
||||
def selected_file(self):
|
||||
return self._selected_project_file
|
||||
|
||||
@property
|
||||
def has_selected_file(self):
|
||||
return self._selected_project_file is not None
|
||||
|
||||
@property
|
||||
def timeout(self):
|
||||
return self._serial_io._read_timeout
|
||||
|
||||
@timeout.setter
|
||||
def timeout(self, value):
|
||||
self._log.debug(f"Setting read timeout to {value}s")
|
||||
self._serial_io._read_timeout = value
|
||||
|
||||
@property
|
||||
def write_timeout(self):
|
||||
return self._serial_io._write_timeout
|
||||
|
||||
@write_timeout.setter
|
||||
def write_timeout(self, value):
|
||||
self._log.debug(f"Setting write timeout to {value}s")
|
||||
self._serial_io._write_timeout = value
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
return "BAMBU"
|
||||
|
||||
@property
|
||||
def baudrate(self):
|
||||
return self._faked_baudrate
|
||||
|
||||
@property
|
||||
def project_files(self):
|
||||
return self._project_files_view
|
||||
|
||||
def change_state(self, new_state: APrinterState):
|
||||
self._state_change_queue.put(new_state)
|
||||
|
||||
def new_update(self, event_type):
|
||||
if event_type == "event_hms_errors":
|
||||
self._update_hms_errors()
|
||||
elif event_type == "event_printer_data_update":
|
||||
self._update_printer_info()
|
||||
|
||||
def _update_printer_info(self):
|
||||
device_data = self.bambu_client.get_device()
|
||||
print_job_state = device_data.print_job.gcode_state
|
||||
temperatures = device_data.temperature
|
||||
# strip out extra data to avoid unneeded settings updates
|
||||
ams_data = [{"tray": asdict(x).pop("tray", None)} for x in device_data.ams.data if x is not None]
|
||||
|
||||
if self.ams_data != ams_data:
|
||||
self._log.debug(f"Recieveid AMS Update: {ams_data}")
|
||||
self.ams_data = ams_data
|
||||
self._settings.set(["ams_data"], ams_data)
|
||||
self._settings.save(trigger_event=True)
|
||||
|
||||
self.lastTempAt = time.monotonic()
|
||||
self._telemetry.temp[0] = temperatures.nozzle_temp
|
||||
self._telemetry.targetTemp[0] = temperatures.target_nozzle_temp
|
||||
self._telemetry.bedTemp = temperatures.bed_temp
|
||||
self._telemetry.bedTargetTemp = temperatures.target_bed_temp
|
||||
self._telemetry.chamberTemp = temperatures.chamber_temp
|
||||
if device_data.push_all_data and "ams" in device_data.push_all_data:
|
||||
self._telemetry.ams_current_tray = device_data.push_all_data["ams"]["tray_now"] or 255
|
||||
|
||||
if self._telemetry.ams_current_tray != self._settings.get_int(["ams_current_tray"]):
|
||||
self._settings.set_int(["ams_current_tray"], self._telemetry.ams_current_tray)
|
||||
self._settings.save(trigger_event=True)
|
||||
|
||||
self._log.debug(f"Received printer state update: {print_job_state}")
|
||||
if (
|
||||
print_job_state == "IDLE"
|
||||
or print_job_state == "FINISH"
|
||||
or print_job_state == "FAILED"
|
||||
):
|
||||
self.change_state(self._state_idle)
|
||||
elif print_job_state == "RUNNING" or print_job_state == "PREPARE":
|
||||
self.change_state(self._state_printing)
|
||||
elif print_job_state == "PAUSE":
|
||||
self.change_state(self._state_paused)
|
||||
else:
|
||||
self._log.warn(f"Unknown print job state: {print_job_state}")
|
||||
|
||||
def _update_hms_errors(self):
|
||||
bambu_printer = self.bambu_client.get_device()
|
||||
if (
|
||||
bambu_printer.hms.errors != self._last_hms_errors
|
||||
and bambu_printer.hms.errors["Count"] > 0
|
||||
):
|
||||
self._log.debug(f"HMS Error: {bambu_printer.hms.errors}")
|
||||
for n in range(1, bambu_printer.hms.errors["Count"] + 1):
|
||||
error = bambu_printer.hms.errors[f"{n}-Error"].strip()
|
||||
self.sendIO(f"// action:notification {error}")
|
||||
self._last_hms_errors = bambu_printer.hms.errors
|
||||
|
||||
def on_disconnect(self, on_disconnect):
|
||||
self._log.debug(f"on disconnect called")
|
||||
self.stop_continuous_status_report()
|
||||
self.stop_continuous_temp_report()
|
||||
return on_disconnect
|
||||
|
||||
def on_connect(self, on_connect):
|
||||
self._log.debug(f"on connect called")
|
||||
return on_connect
|
||||
|
||||
def _create_client_connection_async(self):
|
||||
self._create_client_connection()
|
||||
if self._bambu_client is None:
|
||||
raise RuntimeError("Connection with Bambu Client not established")
|
||||
return self._bambu_client
|
||||
|
||||
def _create_client_connection(self):
|
||||
if (
|
||||
self._settings.get(["device_type"]) == ""
|
||||
or self._settings.get(["serial"]) == ""
|
||||
or self._settings.get(["username"]) == ""
|
||||
or self._settings.get(["access_code"]) == ""
|
||||
):
|
||||
msg = "invalid settings to start connection with Bambu Printer"
|
||||
self._log.debug(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
self._log.debug(
|
||||
f"connecting via local mqtt: {self._settings.get_boolean(['local_mqtt'])}"
|
||||
)
|
||||
bambu_client = BambuClient(
|
||||
{"device_type": self._settings.get(["device_type"]),
|
||||
"serial": self._settings.get(["serial"]),
|
||||
"host": self._settings.get(["host"]),
|
||||
"username": (
|
||||
"bblp"
|
||||
if self._settings.get_boolean(["local_mqtt"])
|
||||
else self._settings.get(["username"])
|
||||
),
|
||||
"access_code": self._settings.get(["access_code"]),
|
||||
"local_mqtt": self._settings.get_boolean(["local_mqtt"]),
|
||||
"region": self._settings.get(["region"]),
|
||||
"email": self._settings.get(["email"]),
|
||||
"auth_token": self._settings.get(["auth_token"]) if self._settings.get_boolean(["local_mqtt"]) is False else "",
|
||||
}
|
||||
)
|
||||
bambu_client.on_disconnect = self.on_disconnect(bambu_client.on_disconnect)
|
||||
bambu_client.on_connect = self.on_connect(bambu_client.on_connect)
|
||||
bambu_client.connect(callback=self.new_update)
|
||||
self._log.info(f"bambu connection status: {bambu_client.connected}")
|
||||
self.sendOk()
|
||||
self._bambu_client = bambu_client
|
||||
|
||||
def __str__(self):
|
||||
return "BAMBU(read_timeout={read_timeout},write_timeout={write_timeout},options={options})".format(
|
||||
read_timeout=self.timeout,
|
||||
write_timeout=self.write_timeout,
|
||||
options={
|
||||
"device_type": self._settings.get(["device_type"]),
|
||||
"host": self._settings.get(["host"]),
|
||||
},
|
||||
)
|
||||
|
||||
def _reset(self):
|
||||
with self._serial_io.incoming_lock:
|
||||
self.lastN = 0
|
||||
self._running = False
|
||||
|
||||
if self._print_status_reporter is not None:
|
||||
self._print_status_reporter.cancel()
|
||||
self._print_status_reporter = None
|
||||
|
||||
if self._settings.get_boolean(["simulateReset"]):
|
||||
for item in self._settings.get(["resetLines"]):
|
||||
self.sendIO(item + "\n")
|
||||
|
||||
self._serial_io.reset()
|
||||
|
||||
def write(self, data: bytes) -> int:
|
||||
return self._serial_io.write(data)
|
||||
|
||||
def readline(self) -> bytes:
|
||||
return self._serial_io.readline()
|
||||
|
||||
def readlines(self) -> list[bytes]:
|
||||
return self._serial_io.readlines()
|
||||
|
||||
def sendIO(self, line: str):
|
||||
self._serial_io.send(line)
|
||||
|
||||
def sendOk(self):
|
||||
self._serial_io.sendOk()
|
||||
|
||||
def flush(self):
|
||||
self._serial_io.flush()
|
||||
self._wait_for_state_change()
|
||||
|
||||
##~~ project file functions
|
||||
|
||||
def remove_project_selection(self):
|
||||
self._selected_project_file = None
|
||||
|
||||
def select_project_file(self, file_path: str) -> bool:
|
||||
file_info = self._project_files_view.get_file_by_name(file_path)
|
||||
if (
|
||||
self._selected_project_file is not None
|
||||
and file_info is not None
|
||||
and self._selected_project_file.path == file_info.path
|
||||
):
|
||||
self._log.debug(f"File already selected: {file_path}")
|
||||
return True
|
||||
|
||||
if file_info is None:
|
||||
self._log.error(f"Cannot select non-existent file: {file_path}")
|
||||
return False
|
||||
|
||||
self._log.debug(f"Select project file: {file_path}")
|
||||
|
||||
self._selected_project_file = file_info
|
||||
self._send_file_selected_message()
|
||||
return True
|
||||
|
||||
##~~ command implementations
|
||||
|
||||
@gcode_executor.register_no_data("M21")
|
||||
def _sd_status(self) -> bool:
|
||||
self.sendIO("SD card ok")
|
||||
return True
|
||||
|
||||
@gcode_executor.register("M23")
|
||||
def _select_sd_file(self, data: str) -> bool:
|
||||
filename = data.split(maxsplit=1)[1].strip()
|
||||
return self.select_project_file(filename)
|
||||
|
||||
def _send_file_selected_message(self):
|
||||
if self.selected_file is None:
|
||||
return
|
||||
|
||||
self.sendIO(
|
||||
f"File opened: {self.selected_file.file_name} "
|
||||
f"Size: {self.selected_file.size}"
|
||||
)
|
||||
self.sendIO("File selected")
|
||||
|
||||
@gcode_executor.register("M26")
|
||||
def _set_sd_position(self, data: str) -> bool:
|
||||
if data == "M26 S0":
|
||||
return self._cancel_print()
|
||||
else:
|
||||
self._log.debug("ignoring M26 command.")
|
||||
self.sendIO("M26 disabled for Bambu")
|
||||
return True
|
||||
|
||||
@gcode_executor.register("M27")
|
||||
def _report_sd_print_status(self, data: str) -> bool:
|
||||
matchS = re.search(r"S([0-9]+)", data)
|
||||
if matchS:
|
||||
interval = int(matchS.group(1))
|
||||
if interval > 0:
|
||||
self.start_continuous_status_report(interval)
|
||||
return False
|
||||
else:
|
||||
self.stop_continuous_status_report()
|
||||
return False
|
||||
|
||||
self.report_print_job_status()
|
||||
return True
|
||||
|
||||
def start_continuous_status_report(self, interval: int):
|
||||
if self._print_status_reporter is not None:
|
||||
self._print_status_reporter.cancel()
|
||||
|
||||
self._print_status_reporter = RepeatedTimer(
|
||||
interval, self.report_print_job_status
|
||||
)
|
||||
self._print_status_reporter.start()
|
||||
|
||||
def stop_continuous_status_report(self):
|
||||
if self._print_status_reporter is not None:
|
||||
self._print_status_reporter.cancel()
|
||||
self._print_status_reporter = None
|
||||
|
||||
@gcode_executor.register("M30")
|
||||
def _delete_project_file(self, data: str) -> bool:
|
||||
file_path = data.split(maxsplit=1)[1].strip()
|
||||
file_info = self.project_files.get_file_data(file_path)
|
||||
if file_info is not None:
|
||||
self.file_system.delete_file(file_info.path)
|
||||
self._update_project_file_list()
|
||||
else:
|
||||
self._log.error(f"File not found to delete {file_path}")
|
||||
return True
|
||||
|
||||
@gcode_executor.register("M105")
|
||||
def _report_temperatures(self, data: str) -> bool:
|
||||
self._processTemperatureQuery()
|
||||
return True
|
||||
|
||||
@gcode_executor.register("M155")
|
||||
def _auto_report_temperatures(self, data: str) -> bool:
|
||||
matchS = re.search(r"S([0-9]+)", data)
|
||||
if matchS:
|
||||
interval = int(matchS.group(1))
|
||||
if interval > 0:
|
||||
self.start_continuous_temp_report(interval)
|
||||
else:
|
||||
self.stop_continuous_temp_report()
|
||||
|
||||
self.report_print_job_status()
|
||||
return True
|
||||
|
||||
def start_continuous_temp_report(self, interval: int):
|
||||
if self._print_temp_reporter is not None:
|
||||
self._print_temp_reporter.cancel()
|
||||
|
||||
self._print_temp_reporter = RepeatedTimer(
|
||||
interval, self._processTemperatureQuery
|
||||
)
|
||||
self._print_temp_reporter.start()
|
||||
|
||||
def stop_continuous_temp_report(self):
|
||||
if self._print_temp_reporter is not None:
|
||||
self._print_temp_reporter.cancel()
|
||||
self._print_temp_reporter = None
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@gcode_executor.register_no_data("M115")
|
||||
def _report_firmware_info(self) -> bool:
|
||||
# wait for connection to be established before sending back firmware info
|
||||
while self.bambu_client.connected is False:
|
||||
time.sleep(1)
|
||||
self.sendIO("Bambu Printer Integration")
|
||||
self.sendIO("Cap:AUTOREPORT_SD_STATUS:1")
|
||||
self.sendIO("Cap:AUTOREPORT_TEMP:1")
|
||||
self.sendIO("Cap:EXTENDED_M20:1")
|
||||
self.sendIO("Cap:LFN_WRITE:1")
|
||||
return True
|
||||
|
||||
@gcode_executor.register("M117")
|
||||
def _get_lcd_message(self, data: str) -> bool:
|
||||
result = re.search(r"M117\s+(.*)", data).group(1)
|
||||
self.sendIO(f"echo:{result}")
|
||||
return True
|
||||
|
||||
@gcode_executor.register("M118")
|
||||
def _serial_print(self, data: str) -> bool:
|
||||
match = re.search(r"M118 (?:(?P<parameter>A1|E1|Pn[012])\s)?(?P<text>.*)", data)
|
||||
if not match:
|
||||
self.sendIO("Unrecognized command parameters for M118")
|
||||
else:
|
||||
result = match.groupdict()
|
||||
text = result["text"]
|
||||
parameter = result["parameter"]
|
||||
|
||||
if parameter == "A1":
|
||||
self.sendIO(f"//{text}")
|
||||
elif parameter == "E1":
|
||||
self.sendIO(f"echo:{text}")
|
||||
else:
|
||||
self.sendIO(text)
|
||||
return True
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@gcode_executor.register("M220")
|
||||
def _set_feedrate_percent(self, data: str) -> bool:
|
||||
if self.bambu_client.connected:
|
||||
gcode_command = commands.SEND_GCODE_TEMPLATE
|
||||
percent = int(data.replace("M220 S", ""))
|
||||
|
||||
def speed_fraction(speed_percent):
|
||||
return math.floor(10000 / speed_percent) / 100
|
||||
|
||||
def acceleration_magnitude(speed_percent):
|
||||
return math.exp((speed_fraction(speed_percent) - 1.0191) / -0.8139)
|
||||
|
||||
def feed_rate(speed_percent):
|
||||
return 6.426e-5 * speed_percent ** 2 - 2.484e-3 * speed_percent + 0.654
|
||||
|
||||
def linear_interpolate(x, x_points, y_points):
|
||||
if x <= x_points[0]: return y_points[0]
|
||||
if x >= x_points[-1]: return y_points[-1]
|
||||
for i in range(len(x_points) - 1):
|
||||
if x_points[i] <= x < x_points[i + 1]:
|
||||
t = (x - x_points[i]) / (x_points[i + 1] - x_points[i])
|
||||
return y_points[i] * (1 - t) + y_points[i + 1] * t
|
||||
|
||||
def scale_to_data_points(func, data_points):
|
||||
data_points.sort(key=lambda x: x[0])
|
||||
speeds, values = zip(*data_points)
|
||||
scaling_factors = [v / func(s) for s, v in zip(speeds, values)]
|
||||
return lambda x: func(x) * linear_interpolate(x, speeds, scaling_factors)
|
||||
|
||||
def speed_adjust(speed_percentage):
|
||||
if not 30 <= speed_percentage <= 180:
|
||||
speed_percentage = 100
|
||||
|
||||
bambu_params = {
|
||||
"speed": [50, 100, 124, 166],
|
||||
"acceleration": [0.3, 1.0, 1.4, 1.6],
|
||||
"feed_rate": [0.7, 1.0, 1.4, 2.0]
|
||||
}
|
||||
|
||||
acc_mag_scaled = scale_to_data_points(acceleration_magnitude,
|
||||
list(zip(bambu_params["speed"], bambu_params["acceleration"])))
|
||||
feed_rate_scaled = scale_to_data_points(feed_rate,
|
||||
list(zip(bambu_params["speed"], bambu_params["feed_rate"])))
|
||||
|
||||
speed_frac = speed_fraction(speed_percentage)
|
||||
acc_mag = acc_mag_scaled(speed_percentage)
|
||||
feed = feed_rate_scaled(speed_percentage)
|
||||
# speed_level = 1.539 * (acc_mag**2) - 0.7032 * acc_mag + 4.0834
|
||||
return f"M204.2 K{acc_mag:.2f}\nM220 K{feed:.2f}\nM73.2 R{speed_frac:.2f}\n" # M1002 set_gcode_claim_speed_level ${speed_level:.0f}\n
|
||||
|
||||
speed_command = speed_adjust(percent)
|
||||
|
||||
gcode_command["print"]["param"] = speed_command
|
||||
if self.bambu_client.publish(gcode_command):
|
||||
self._log.info(f"{percent}% speed adjustment command sent successfully")
|
||||
return True
|
||||
|
||||
def _process_gcode_serial_command(self, gcode: str, full_command: str):
|
||||
self._log.debug(f"processing gcode {gcode} command = {full_command}")
|
||||
handled = self.gcode_executor.execute(self, gcode, full_command)
|
||||
if handled:
|
||||
self.sendOk()
|
||||
return
|
||||
|
||||
# post gcode to printer otherwise
|
||||
if self.bambu_client.connected:
|
||||
GCODE_COMMAND = commands.SEND_GCODE_TEMPLATE
|
||||
GCODE_COMMAND["print"]["param"] = full_command + "\n"
|
||||
if self.bambu_client.publish(GCODE_COMMAND):
|
||||
self._log.info("command sent successfully")
|
||||
self.sendOk()
|
||||
|
||||
@gcode_executor.register_no_data("M112")
|
||||
def _shutdown(self):
|
||||
self._running = True
|
||||
if self.bambu_client.connected:
|
||||
self.bambu_client.disconnect()
|
||||
self.sendIO("echo:EMERGENCY SHUTDOWN DETECTED. KILLED.")
|
||||
self._serial_io.close()
|
||||
return True
|
||||
|
||||
@gcode_executor.register("M20")
|
||||
def _update_project_file_list(self, data: str = ""):
|
||||
self._project_files_view.update() # internally sends list to serial io
|
||||
return True
|
||||
|
||||
def _list_cached_project_files(self):
|
||||
self.sendIO("Begin file list")
|
||||
for item in map(
|
||||
FileInfo.get_gcode_info, self._project_files_view.get_all_cached_info()
|
||||
):
|
||||
self.sendIO(item)
|
||||
self.sendIO("End file list")
|
||||
self.sendOk()
|
||||
|
||||
@gcode_executor.register_no_data("M24")
|
||||
def _start_resume_sd_print(self):
|
||||
self._current_state.start_new_print()
|
||||
return True
|
||||
|
||||
@gcode_executor.register_no_data("M25")
|
||||
def _pause_print(self):
|
||||
self._current_state.pause_print()
|
||||
return True
|
||||
|
||||
@gcode_executor.register("M524")
|
||||
def _cancel_print(self):
|
||||
self._current_state.cancel_print()
|
||||
return True
|
||||
|
||||
def report_print_job_status(self):
|
||||
if self.current_print_job is not None:
|
||||
file_position = 1 if self.current_print_job.file_position == 0 else self.current_print_job.file_position
|
||||
self.sendIO(
|
||||
f"SD printing byte {file_position}"
|
||||
f"/{self.current_print_job.file_info.size}"
|
||||
)
|
||||
else:
|
||||
self.sendIO("Not SD printing")
|
||||
|
||||
def report_print_finished(self):
|
||||
if self.current_print_job is None:
|
||||
return
|
||||
self._log.debug(
|
||||
f"SD File Print finishing: {self.current_print_job.file_info.file_name}"
|
||||
)
|
||||
self.sendIO("Done printing file")
|
||||
|
||||
def finalize_print_job(self):
|
||||
if self.current_print_job is not None:
|
||||
self.report_print_job_status()
|
||||
self.report_print_finished()
|
||||
self.current_print_job = None
|
||||
self.report_print_job_status()
|
||||
self.change_state(self._state_idle)
|
||||
|
||||
def _create_temperature_message(self) -> str:
|
||||
template = "{heater}:{actual:.2f}/ {target:.2f}"
|
||||
temps = collections.OrderedDict()
|
||||
temps["T"] = (self._telemetry.temp[0], self._telemetry.targetTemp[0])
|
||||
temps["B"] = (self._telemetry.bedTemp, self._telemetry.bedTargetTemp)
|
||||
if self._telemetry.hasChamber:
|
||||
temps["C"] = (
|
||||
self._telemetry.chamberTemp,
|
||||
self._telemetry.chamberTargetTemp,
|
||||
)
|
||||
|
||||
output = " ".join(
|
||||
map(
|
||||
lambda x: template.format(heater=x[0], actual=x[1][0], target=x[1][1]),
|
||||
temps.items(),
|
||||
)
|
||||
)
|
||||
output += " @:64\n"
|
||||
return output
|
||||
|
||||
def _processTemperatureQuery(self) -> bool:
|
||||
# includeOk = not self._okBeforeCommandOutput
|
||||
if self.bambu_client.connected:
|
||||
output = self._create_temperature_message()
|
||||
self.sendIO(output)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
if self.bambu_client.connected:
|
||||
self.bambu_client.disconnect()
|
||||
self.change_state(self._state_idle)
|
||||
self._serial_io.close()
|
||||
self.stop()
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
self._printer_thread.join()
|
||||
|
||||
def _wait_for_state_change(self):
|
||||
self._state_change_queue.join()
|
||||
|
||||
def _printer_worker(self):
|
||||
# self._create_client_connection_async()
|
||||
self.sendIO("Printer connection complete")
|
||||
while self._running:
|
||||
try:
|
||||
next_state = self._state_change_queue.get(timeout=0.01)
|
||||
self._trigger_change_state(next_state)
|
||||
self._state_change_queue.task_done()
|
||||
except queue.Empty:
|
||||
continue
|
||||
except Exception as e:
|
||||
self._state_change_queue.task_done()
|
||||
raise e
|
||||
self._current_state.finalize()
|
||||
|
||||
def _trigger_change_state(self, new_state: APrinterState):
|
||||
if self._current_state == new_state:
|
||||
return
|
||||
self._log.debug(
|
||||
f"Changing state from {self._current_state.__class__.__name__} to {new_state.__class__.__name__}"
|
||||
)
|
||||
|
||||
self._current_state.finalize()
|
||||
self._current_state = new_state
|
||||
self._current_state.init()
|
||||
|
||||
def _showPrompt(self, text, choices):
|
||||
self._hidePrompt()
|
||||
self.sendIO(f"//action:prompt_begin {text}")
|
||||
for choice in choices:
|
||||
self.sendIO(f"//action:prompt_button {choice}")
|
||||
self.sendIO("//action:prompt_show")
|
||||
|
||||
def _hidePrompt(self):
|
||||
self.sendIO("//action:prompt_end")
|
@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from .file_info import FileInfo
|
||||
|
||||
from octoprint.util import get_formatted_size, get_formatted_datetime
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BambuTimelapseFileInfo:
|
||||
bytes: int
|
||||
date: str | None
|
||||
name: str
|
||||
size: str
|
||||
thumbnail: str
|
||||
timestamp: float
|
||||
url: str
|
||||
|
||||
def to_dict(self):
|
||||
return asdict(self)
|
||||
|
||||
@staticmethod
|
||||
def from_file_info(file_info: FileInfo):
|
||||
return BambuTimelapseFileInfo(
|
||||
bytes=file_info.size,
|
||||
date=get_formatted_datetime(file_info.date),
|
||||
name=file_info.file_name,
|
||||
size=get_formatted_size(file_info.size),
|
||||
thumbnail=f"/plugin/bambu_printer/thumbnail/{file_info.path.stem}.jpg",
|
||||
timestamp=file_info.timestamp,
|
||||
url=f"/plugin/bambu_printer/timelapse/{file_info.file_name}",
|
||||
)
|
@ -0,0 +1,98 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import (
|
||||
RemoteSDCardFileList,
|
||||
)
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from octoprint_bambu_printer.printer.file_system.file_info import FileInfo
|
||||
|
||||
|
||||
@dataclass
|
||||
class CachedFileView:
|
||||
file_system: RemoteSDCardFileList
|
||||
folder_view: dict[tuple[str, str | list[str] | None], None] = field(
|
||||
default_factory=dict
|
||||
) # dict preserves order, but set does not. We use only dict keys as storage
|
||||
on_update: Callable[[], None] | None = None
|
||||
|
||||
def __post_init__(self):
|
||||
self._file_alias_cache: dict[str, str] = {}
|
||||
self._file_data_cache: dict[str, FileInfo] = {}
|
||||
|
||||
def with_filter(
|
||||
self, folder: str, extensions: str | list[str] | None = None
|
||||
) -> "CachedFileView":
|
||||
self.folder_view[(folder, extensions)] = None
|
||||
return self
|
||||
|
||||
def list_all_views(self):
|
||||
existing_files: list[str] = []
|
||||
result: list[FileInfo] = []
|
||||
|
||||
with self.file_system.get_ftps_client() as ftp:
|
||||
for key in self.folder_view.keys():
|
||||
result.extend(self.file_system.list_files(*key, ftp, existing_files))
|
||||
return result
|
||||
|
||||
def update(self):
|
||||
file_info_list = self.list_all_views()
|
||||
self._update_file_list_cache(file_info_list)
|
||||
if self.on_update:
|
||||
self.on_update()
|
||||
|
||||
def _update_file_list_cache(self, files: list[FileInfo]):
|
||||
self._file_alias_cache = {info.dosname: info.path.as_posix() for info in files}
|
||||
self._file_data_cache = {info.path.as_posix(): info for info in files}
|
||||
|
||||
def get_all_info(self):
|
||||
self.update()
|
||||
return self.get_all_cached_info()
|
||||
|
||||
def get_all_cached_info(self):
|
||||
return list(self._file_data_cache.values())
|
||||
|
||||
def get_keys_as_list(self):
|
||||
return list(self._file_data_cache.keys()) + list(self._file_alias_cache.keys())
|
||||
|
||||
def get_file_data(self, file_path: str | Path) -> FileInfo | None:
|
||||
file_data = self.get_file_data_cached(file_path)
|
||||
if file_data is None:
|
||||
self.update()
|
||||
file_data = self.get_file_data_cached(file_path)
|
||||
return file_data
|
||||
|
||||
def get_file_data_cached(self, file_path: str | Path) -> FileInfo | None:
|
||||
if isinstance(file_path, str):
|
||||
file_path = Path(file_path).as_posix().strip("/")
|
||||
else:
|
||||
file_path = file_path.as_posix().strip("/")
|
||||
|
||||
if file_path not in self._file_data_cache:
|
||||
file_path = self._file_alias_cache.get(file_path, file_path)
|
||||
return self._file_data_cache.get(file_path, None)
|
||||
|
||||
def get_file_by_name(self, file_name: str):
|
||||
if file_name == "":
|
||||
return None
|
||||
|
||||
file_list = self.get_keys_as_list()
|
||||
if not file_name in file_list:
|
||||
if f"{file_name}.3mf" in file_list:
|
||||
file_name = f"{file_name}.3mf"
|
||||
elif f"{file_name}.gcode.3mf" in file_list:
|
||||
file_name = f"{file_name}.gcode.3mf"
|
||||
elif f"cache/{file_name}.3mf" in file_list:
|
||||
file_name = f"cache/{file_name}.3mf"
|
||||
elif f"cache/{file_name}.gcode.3mf" in file_list:
|
||||
file_name = f"cache/{file_name}.gcode.3mf"
|
||||
|
||||
file_data = self.get_file_data_cached(file_name)
|
||||
if file_data is None:
|
||||
self.update()
|
||||
return self.get_file_by_name(file_name)
|
||||
return file_data
|
33
octoprint_bambu_printer/printer/file_system/file_info.py
Normal file
33
octoprint_bambu_printer/printer/file_system/file_info.py
Normal file
@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from octoprint.util.files import unix_timestamp_to_m20_timestamp
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FileInfo:
|
||||
dosname: str
|
||||
path: Path
|
||||
size: int
|
||||
date: datetime
|
||||
|
||||
@property
|
||||
def file_name(self):
|
||||
return self.path.name
|
||||
|
||||
@property
|
||||
def timestamp(self) -> float:
|
||||
return self.date.timestamp()
|
||||
|
||||
@property
|
||||
def timestamp_m20(self) -> str:
|
||||
return unix_timestamp_to_m20_timestamp(int(self.timestamp))
|
||||
|
||||
def get_gcode_info(self) -> str:
|
||||
return f'{self.dosname} {self.size} {self.timestamp_m20} "{self.file_name}"'
|
||||
|
||||
def to_dict(self):
|
||||
return asdict(self)
|
256
octoprint_bambu_printer/printer/file_system/ftps_client.py
Normal file
256
octoprint_bambu_printer/printer/file_system/ftps_client.py
Normal file
@ -0,0 +1,256 @@
|
||||
"""
|
||||
Based on: <https://github.com/dgonzo27/py-iot-utils>
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
wrapper for FTPS server interactions
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
import ftplib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import ssl
|
||||
from typing import Generator, Union
|
||||
|
||||
from contextlib import redirect_stdout
|
||||
import io
|
||||
import re
|
||||
|
||||
|
||||
class ImplicitTLS(ftplib.FTP_TLS):
|
||||
"""ftplib.FTP_TLS sub-class to support implicit SSL FTPS"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._sock = None
|
||||
|
||||
@property
|
||||
def sock(self):
|
||||
"""return socket"""
|
||||
return self._sock
|
||||
|
||||
@sock.setter
|
||||
def sock(self, value):
|
||||
"""wrap and set SSL socket"""
|
||||
if value is not None and not isinstance(value, ssl.SSLSocket):
|
||||
value = self.context.wrap_socket(value)
|
||||
self._sock = value
|
||||
|
||||
def ntransfercmd(self, cmd, rest=None):
|
||||
conn, size = ftplib.FTP.ntransfercmd(self, cmd, rest)
|
||||
|
||||
if self._prot_p:
|
||||
conn = self.context.wrap_socket(
|
||||
conn, server_hostname=self.host, session=self.sock.session
|
||||
) # this is the fix
|
||||
return conn, size
|
||||
|
||||
|
||||
@dataclass
|
||||
class IoTFTPSConnection:
|
||||
"""iot ftps ftpsclient"""
|
||||
|
||||
ftps_session: ftplib.FTP | ImplicitTLS
|
||||
|
||||
def close(self) -> None:
|
||||
"""close the current session from the ftps server"""
|
||||
self.ftps_session.close()
|
||||
|
||||
def download_file(self, source: str, dest: str):
|
||||
"""download a file to a path on the local filesystem"""
|
||||
with open(dest, "wb") as file:
|
||||
self.ftps_session.retrbinary(f"RETR {source}", file.write)
|
||||
|
||||
def upload_file(self, source: str, dest: str, callback=None) -> bool:
|
||||
"""upload a file to a path inside the FTPS server"""
|
||||
|
||||
file_size = os.path.getsize(source)
|
||||
|
||||
block_size = max(file_size // 100, 8192)
|
||||
rest = None
|
||||
|
||||
try:
|
||||
# Taken from ftplib.storbinary but with custom ssl handling
|
||||
# due to the shitty bambu p1p ftps server TODO fix properly.
|
||||
with open(source, "rb") as fp:
|
||||
self.ftps_session.voidcmd("TYPE I")
|
||||
|
||||
with self.ftps_session.transfercmd(f"STOR {dest}", rest) as conn:
|
||||
while 1:
|
||||
buf = fp.read(block_size)
|
||||
|
||||
if not buf:
|
||||
break
|
||||
|
||||
conn.sendall(buf)
|
||||
|
||||
if callback:
|
||||
callback(buf)
|
||||
|
||||
# shutdown ssl layer
|
||||
if ftplib._SSLSocket is not None and isinstance(
|
||||
conn, ftplib._SSLSocket
|
||||
):
|
||||
# Yeah this is suposed to be conn.unwrap
|
||||
# But since we operate in prot p mode
|
||||
# we can close the connection always.
|
||||
# This is cursed but it works.
|
||||
if "vsFTPd" in self.ftps_session.welcome:
|
||||
conn.unwrap()
|
||||
else:
|
||||
conn.shutdown(socket.SHUT_RDWR)
|
||||
|
||||
return True
|
||||
except Exception as ex:
|
||||
print(f"unexpected exception occurred: {ex}")
|
||||
pass
|
||||
return False
|
||||
|
||||
def delete_file(self, path: str) -> bool:
|
||||
"""delete a file from under a path inside the FTPS server"""
|
||||
try:
|
||||
self.ftps_session.delete(path)
|
||||
return True
|
||||
except Exception as ex:
|
||||
print(f"unexpected exception occurred: {ex}")
|
||||
pass
|
||||
return False
|
||||
|
||||
def move_file(self, source: str, dest: str):
|
||||
"""move a file inside the FTPS server to another path inside the FTPS server"""
|
||||
self.ftps_session.rename(source, dest)
|
||||
|
||||
def mkdir(self, path: str) -> str:
|
||||
return self.ftps_session.mkd(path)
|
||||
|
||||
def list_files(
|
||||
self, list_path: str, extensions: str | list[str] | None = None
|
||||
) -> Generator[Path]:
|
||||
"""list files under a path inside the FTPS server"""
|
||||
|
||||
if extensions is None:
|
||||
_extension_acceptable = lambda p: True
|
||||
else:
|
||||
if isinstance(extensions, str):
|
||||
extensions = [extensions]
|
||||
_extension_acceptable = lambda p: any(s in p.suffixes for s in extensions)
|
||||
|
||||
try:
|
||||
list_result = self.ftps_session.nlst(list_path) or []
|
||||
for file_list_entry in list_result:
|
||||
path = Path(list_path) / Path(file_list_entry).name
|
||||
if _extension_acceptable(path):
|
||||
yield path
|
||||
except Exception as ex:
|
||||
print(f"unexpected exception occurred: {ex}")
|
||||
|
||||
def list_files_ex(self, path: str) -> Union[list[str], None]:
|
||||
"""list files under a path inside the FTPS server"""
|
||||
try:
|
||||
f = io.StringIO()
|
||||
with redirect_stdout(f):
|
||||
self.ftps_session.dir(path)
|
||||
s = f.getvalue()
|
||||
files = []
|
||||
for row in s.split("\n"):
|
||||
if len(row) <= 0:
|
||||
continue
|
||||
|
||||
attribs = row.split(" ")
|
||||
|
||||
match = re.search(r".*\ (\d\d\:\d\d|\d\d\d\d)\ (.*)", row)
|
||||
name = ""
|
||||
if match:
|
||||
name = match.groups(1)[1]
|
||||
else:
|
||||
name = attribs[len(attribs) - 1]
|
||||
|
||||
file = (attribs[0], name)
|
||||
files.append(file)
|
||||
return files
|
||||
except Exception as ex:
|
||||
print(f"unexpected exception occurred: [{ex}]")
|
||||
pass
|
||||
return
|
||||
|
||||
def get_file_size(self, file_path: str):
|
||||
try:
|
||||
return self.ftps_session.size(file_path)
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
f'Cannot get file size for "{file_path}" due to error: {str(e)}'
|
||||
)
|
||||
|
||||
def get_file_date(self, file_path: str) -> datetime:
|
||||
try:
|
||||
date_response = self.ftps_session.sendcmd(f"MDTM {file_path}").replace(
|
||||
"213 ", ""
|
||||
)
|
||||
date = datetime.strptime(date_response, "%Y%m%d%H%M%S").replace(
|
||||
tzinfo=timezone.utc
|
||||
)
|
||||
return date
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
f'Cannot get file date for "{file_path}" due to error: {str(e)}'
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class IoTFTPSClient:
|
||||
ftps_host: str
|
||||
ftps_port: int = 21
|
||||
ftps_user: str = ""
|
||||
ftps_pass: str = ""
|
||||
ssl_implicit: bool = False
|
||||
welcome: str = ""
|
||||
_connection: IoTFTPSConnection | None = None
|
||||
|
||||
def __enter__(self):
|
||||
session = self.open_ftps_session()
|
||||
self._connection = IoTFTPSConnection(session)
|
||||
return self._connection
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
if self._connection is not None:
|
||||
self._connection.close()
|
||||
self._connection = None
|
||||
|
||||
def open_ftps_session(self) -> ftplib.FTP | ImplicitTLS:
|
||||
"""init ftps_session based on input params"""
|
||||
ftps_session = ImplicitTLS() if self.ssl_implicit else ftplib.FTP()
|
||||
ftps_session.set_debuglevel(0)
|
||||
|
||||
self.welcome = ftps_session.connect(host=self.ftps_host, port=self.ftps_port)
|
||||
|
||||
if self.ftps_user and self.ftps_pass:
|
||||
ftps_session.login(user=self.ftps_user, passwd=self.ftps_pass)
|
||||
else:
|
||||
ftps_session.login()
|
||||
|
||||
if self.ssl_implicit:
|
||||
ftps_session.prot_p()
|
||||
|
||||
return ftps_session
|
@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Iterator
|
||||
import logging.handlers
|
||||
|
||||
from octoprint.util import get_dos_filename
|
||||
|
||||
from .ftps_client import IoTFTPSClient, IoTFTPSConnection
|
||||
from .file_info import FileInfo
|
||||
|
||||
|
||||
class RemoteSDCardFileList:
|
||||
|
||||
def __init__(self, settings) -> None:
|
||||
self._settings = settings
|
||||
self._selected_project_file: FileInfo | None = None
|
||||
self._logger = logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter")
|
||||
|
||||
def delete_file(self, file_path: Path) -> None:
|
||||
try:
|
||||
with self.get_ftps_client() as ftp:
|
||||
if ftp.delete_file(file_path.as_posix()):
|
||||
self._logger.debug(f"{file_path} deleted")
|
||||
else:
|
||||
raise RuntimeError(f"Deleting file {file_path} failed")
|
||||
except Exception as e:
|
||||
self._logger.exception(e)
|
||||
|
||||
def list_files(
|
||||
self,
|
||||
folder: str,
|
||||
extensions: str | list[str] | None,
|
||||
ftp: IoTFTPSConnection,
|
||||
existing_files=None,
|
||||
):
|
||||
if existing_files is None:
|
||||
existing_files = []
|
||||
|
||||
return list(
|
||||
self.get_file_info_for_names(
|
||||
ftp, ftp.list_files(folder, extensions), existing_files
|
||||
)
|
||||
)
|
||||
|
||||
def _get_ftp_file_info(
|
||||
self,
|
||||
ftp: IoTFTPSConnection,
|
||||
file_path: Path,
|
||||
existing_files: list[str] | None = None,
|
||||
):
|
||||
file_size = ftp.get_file_size(file_path.as_posix())
|
||||
date = ftp.get_file_date(file_path.as_posix())
|
||||
file_name = file_path.name.lower()
|
||||
dosname = get_dos_filename(file_name, existing_filenames=existing_files).lower()
|
||||
return FileInfo(
|
||||
dosname,
|
||||
file_path,
|
||||
file_size if file_size is not None else 0,
|
||||
date,
|
||||
)
|
||||
|
||||
def get_file_info_for_names(
|
||||
self,
|
||||
ftp: IoTFTPSConnection,
|
||||
files: Iterable[Path],
|
||||
existing_files: list[str] | None = None,
|
||||
) -> Iterator[FileInfo]:
|
||||
if existing_files is None:
|
||||
existing_files = []
|
||||
|
||||
for entry in files:
|
||||
try:
|
||||
file_info = self._get_ftp_file_info(ftp, entry, existing_files)
|
||||
yield file_info
|
||||
existing_files.append(file_info.file_name)
|
||||
existing_files.append(file_info.dosname)
|
||||
except Exception as e:
|
||||
self._logger.exception(e, exc_info=False)
|
||||
|
||||
def get_ftps_client(self):
|
||||
host = self._settings.get(["host"])
|
||||
access_code = self._settings.get(["access_code"])
|
||||
return IoTFTPSClient(
|
||||
f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True
|
||||
)
|
319
octoprint_bambu_printer/printer/gcode_executor.py
Normal file
319
octoprint_bambu_printer/printer/gcode_executor.py
Normal file
@ -0,0 +1,319 @@
|
||||
import itertools
|
||||
import logging
|
||||
from inspect import signature
|
||||
import traceback
|
||||
|
||||
|
||||
GCODE_DOCUMENTATION = {
|
||||
"G0": "Linear Move",
|
||||
"G1": "Linear Move",
|
||||
"G2": "Arc or Circle Move",
|
||||
"G3": "Arc or Circle Move",
|
||||
"G4": "Dwell",
|
||||
"G5": "Bézier cubic spline",
|
||||
"G6": "Direct Stepper Move",
|
||||
"G10": "Retract",
|
||||
"G11": "Recover",
|
||||
"G12": "Clean the Nozzle",
|
||||
"G17": "CNC Workspace Planes",
|
||||
"G18": "CNC Workspace Planes",
|
||||
"G19": "CNC Workspace Planes",
|
||||
"G20": "Inch Units",
|
||||
"G21": "Millimeter Units",
|
||||
"G26": "Mesh Validation Pattern",
|
||||
"G27": "Park toolhead",
|
||||
"G28": "Auto Home",
|
||||
"G29": "Bed Leveling",
|
||||
"G29": "Bed Leveling (3-Point)",
|
||||
"G29": "Bed Leveling (Linear)",
|
||||
"G29": "Bed Leveling (Manual)",
|
||||
"G29": "Bed Leveling (Bilinear)",
|
||||
"G29": "Bed Leveling (Unified)",
|
||||
"G30": "Single Z-Probe",
|
||||
"G31": "Dock Sled",
|
||||
"G32": "Undock Sled",
|
||||
"G33": "Delta Auto Calibration",
|
||||
"G34": "Z Steppers Auto-Alignment",
|
||||
"G34": "Mechanical Gantry Calibration",
|
||||
"G35": "Tramming Assistant",
|
||||
"G38.2": "Probe target",
|
||||
"G38.3": "Probe target",
|
||||
"G38.4": "Probe target",
|
||||
"G38.5": "Probe target",
|
||||
"G42": "Move to mesh coordinate",
|
||||
"G53": "Move in Machine Coordinates",
|
||||
"G60": "Save Current Position",
|
||||
"G61": "Return to Saved Position",
|
||||
"G76": "Probe temperature calibration",
|
||||
"G80": "Cancel Current Motion Mode",
|
||||
"G90": "Absolute Positioning",
|
||||
"G91": "Relative Positioning",
|
||||
"G92": "Set Position",
|
||||
"G425": "Backlash Calibration",
|
||||
"M0": "Unconditional stop",
|
||||
"M1": "Unconditional stop",
|
||||
"M3": "Spindle CW / Laser On",
|
||||
"M4": "Spindle CCW / Laser On",
|
||||
"M5": "Spindle / Laser Off",
|
||||
"M7": "Coolant Controls",
|
||||
"M8": "Coolant Controls",
|
||||
"M9": "Coolant Controls",
|
||||
"M10": "Vacuum / Blower Control",
|
||||
"M11": "Vacuum / Blower Control",
|
||||
"M16": "Expected Printer Check",
|
||||
"M17": "Enable Steppers",
|
||||
"M18": "Disable steppers",
|
||||
"M84": "Disable steppers",
|
||||
"M20": "List SD Card",
|
||||
"M21": "Init SD card",
|
||||
"M22": "Release SD card",
|
||||
"M23": "Select SD file",
|
||||
"M24": "Start or Resume SD print",
|
||||
"M25": "Pause SD print",
|
||||
"M26": "Set SD position",
|
||||
"M27": "Report SD print status",
|
||||
"M28": "Start SD write",
|
||||
"M29": "Stop SD write",
|
||||
"M30": "Delete SD file",
|
||||
"M31": "Print time",
|
||||
"M32": "Select and Start",
|
||||
"M33": "Get Long Path",
|
||||
"M34": "SDCard Sorting",
|
||||
"M42": "Set Pin State",
|
||||
"M43": "Debug Pins",
|
||||
"M48": "Probe Repeatability Test",
|
||||
"M73": "Set Print Progress",
|
||||
"M75": "Start Print Job Timer",
|
||||
"M76": "Pause Print Job Timer",
|
||||
"M77": "Stop Print Job Timer",
|
||||
"M78": "Print Job Stats",
|
||||
"M80": "Power On",
|
||||
"M81": "Power Off",
|
||||
"M82": "E Absolute",
|
||||
"M83": "E Relative",
|
||||
"M85": "Inactivity Shutdown",
|
||||
"M86": "Hotend Idle Timeout",
|
||||
"M87": "Disable Hotend Idle Timeout",
|
||||
"M92": "Set Axis Steps-per-unit",
|
||||
"M100": "Free Memory",
|
||||
"M102": "Configure Bed Distance Sensor",
|
||||
"M104": "Set Hotend Temperature",
|
||||
"M105": "Report Temperatures",
|
||||
"M106": "Set Fan Speed",
|
||||
"M107": "Fan Off",
|
||||
"M108": "Break and Continue",
|
||||
"M109": "Wait for Hotend Temperature",
|
||||
"M110": "Set / Get Line Number",
|
||||
"M111": "Debug Level",
|
||||
"M112": "Full Shutdown",
|
||||
"M113": "Host Keepalive",
|
||||
"M114": "Get Current Position",
|
||||
"M115": "Firmware Info",
|
||||
"M117": "Set LCD Message",
|
||||
"M118": "Serial print",
|
||||
"M119": "Endstop States",
|
||||
"M120": "Enable Endstops",
|
||||
"M121": "Disable Endstops",
|
||||
"M122": "TMC Debugging",
|
||||
"M123": "Fan Tachometers",
|
||||
"M125": "Park Head",
|
||||
"M126": "Baricuda 1 Open",
|
||||
"M127": "Baricuda 1 Close",
|
||||
"M128": "Baricuda 2 Open",
|
||||
"M129": "Baricuda 2 Close",
|
||||
"M140": "Set Bed Temperature",
|
||||
"M141": "Set Chamber Temperature",
|
||||
"M143": "Set Laser Cooler Temperature",
|
||||
"M145": "Set Material Preset",
|
||||
"M149": "Set Temperature Units",
|
||||
"M150": "Set RGB(W) Color",
|
||||
"M154": "Position Auto-Report",
|
||||
"M155": "Temperature Auto-Report",
|
||||
"M163": "Set Mix Factor",
|
||||
"M164": "Save Mix",
|
||||
"M165": "Set Mix",
|
||||
"M166": "Gradient Mix",
|
||||
"M190": "Wait for Bed Temperature",
|
||||
"M191": "Wait for Chamber Temperature",
|
||||
"M192": "Wait for Probe temperature",
|
||||
"M193": "Set Laser Cooler Temperature",
|
||||
"M200": "Set Filament Diameter",
|
||||
"M201": "Print / Travel Move Limits",
|
||||
"M203": "Set Max Feedrate",
|
||||
"M204": "Set Starting Acceleration",
|
||||
"M205": "Set Advanced Settings",
|
||||
"M206": "Set Home Offsets",
|
||||
"M207": "Set Firmware Retraction",
|
||||
"M208": "Firmware Recover",
|
||||
"M209": "Set Auto Retract",
|
||||
"M211": "Software Endstops",
|
||||
"M217": "Filament swap parameters",
|
||||
"M218": "Set Hotend Offset",
|
||||
"M220": "Set Feedrate Percentage",
|
||||
"M221": "Set Flow Percentage",
|
||||
"M226": "Wait for Pin State",
|
||||
"M240": "Trigger Camera",
|
||||
"M250": "LCD Contrast",
|
||||
"M255": "LCD Sleep/Backlight Timeout",
|
||||
"M256": "LCD Brightness",
|
||||
"M260": "I2C Send",
|
||||
"M261": "I2C Request",
|
||||
"M280": "Servo Position",
|
||||
"M281": "Edit Servo Angles",
|
||||
"M282": "Detach Servo",
|
||||
"M290": "Babystep",
|
||||
"M300": "Play Tone",
|
||||
"M301": "Set Hotend PID",
|
||||
"M302": "Cold Extrude",
|
||||
"M303": "PID autotune",
|
||||
"M304": "Set Bed PID",
|
||||
"M305": "User Thermistor Parameters",
|
||||
"M306": "Model Predictive Temp. Control",
|
||||
"M350": "Set micro-stepping",
|
||||
"M351": "Set Microstep Pins",
|
||||
"M355": "Case Light Control",
|
||||
"M360": "SCARA Theta A",
|
||||
"M361": "SCARA Theta-B",
|
||||
"M362": "SCARA Psi-A",
|
||||
"M363": "SCARA Psi-B",
|
||||
"M364": "SCARA Psi-C",
|
||||
"M380": "Activate Solenoid",
|
||||
"M381": "Deactivate Solenoids",
|
||||
"M400": "Finish Moves",
|
||||
"M401": "Deploy Probe",
|
||||
"M402": "Stow Probe",
|
||||
"M403": "MMU2 Filament Type",
|
||||
"M404": "Set Filament Diameter",
|
||||
"M405": "Filament Width Sensor On",
|
||||
"M406": "Filament Width Sensor Off",
|
||||
"M407": "Filament Width",
|
||||
"M410": "Quickstop",
|
||||
"M412": "Filament Runout",
|
||||
"M413": "Power-loss Recovery",
|
||||
"M420": "Bed Leveling State",
|
||||
"M421": "Set Mesh Value",
|
||||
"M422": "Set Z Motor XY",
|
||||
"M423": "X Twist Compensation",
|
||||
"M425": "Backlash compensation",
|
||||
"M428": "Home Offsets Here",
|
||||
"M430": "Power Monitor",
|
||||
"M486": "Cancel Objects",
|
||||
"M493": "Fixed-Time Motion",
|
||||
"M500": "Save Settings",
|
||||
"M501": "Restore Settings",
|
||||
"M502": "Factory Reset",
|
||||
"M503": "Report Settings",
|
||||
"M504": "Validate EEPROM contents",
|
||||
"M510": "Lock Machine",
|
||||
"M511": "Unlock Machine",
|
||||
"M512": "Set Passcode",
|
||||
"M524": "Abort SD print",
|
||||
"M540": "Endstops Abort SD",
|
||||
"M569": "Set TMC stepping mode",
|
||||
"M575": "Serial baud rate",
|
||||
"M592": "Nonlinear Extrusion Control",
|
||||
"M593": "ZV Input Shaping",
|
||||
"M600": "Filament Change",
|
||||
"M603": "Configure Filament Change",
|
||||
"M605": "Multi Nozzle Mode",
|
||||
"M665": "Delta Configuration",
|
||||
"M665": "SCARA Configuration",
|
||||
"M666": "Set Delta endstop adjustments",
|
||||
"M666": "Set dual endstop offsets",
|
||||
"M672": "Duet Smart Effector sensitivity",
|
||||
"M701": "Load filament",
|
||||
"M702": "Unload filament",
|
||||
"M710": "Controller Fan settings",
|
||||
"M808": "Repeat Marker",
|
||||
"M851": "XYZ Probe Offset",
|
||||
"M852": "Bed Skew Compensation",
|
||||
"M871": "Probe temperature config",
|
||||
"M876": "Handle Prompt Response",
|
||||
"M900": "Linear Advance Factor",
|
||||
"M906": "Stepper Motor Current",
|
||||
"M907": "Set Motor Current",
|
||||
"M908": "Set Trimpot Pins",
|
||||
"M909": "DAC Print Values",
|
||||
"M910": "Commit DAC to EEPROM",
|
||||
"M911": "TMC OT Pre-Warn Condition",
|
||||
"M912": "Clear TMC OT Pre-Warn",
|
||||
"M913": "Set Hybrid Threshold Speed",
|
||||
"M914": "TMC Bump Sensitivity",
|
||||
"M915": "TMC Z axis calibration",
|
||||
"M916": "L6474 Thermal Warning Test",
|
||||
"M917": "L6474 Overcurrent Warning Test",
|
||||
"M918": "L6474 Speed Warning Test",
|
||||
"M919": "TMC Chopper Timing",
|
||||
"M928": "Start SD Logging",
|
||||
"M951": "Magnetic Parking Extruder",
|
||||
"M993": "Back up flash settings to SD",
|
||||
"M994": "Restore flash from SD",
|
||||
"M995": "Touch Screen Calibration",
|
||||
"M997": "Firmware update",
|
||||
"M999": "STOP Restart",
|
||||
"M7219": "MAX7219 Control",
|
||||
}
|
||||
|
||||
|
||||
class GCodeExecutor:
|
||||
def __init__(self):
|
||||
self._log = logging.getLogger(
|
||||
"octoprint.plugins.bambu_printer.BambuPrinter.gcode_executor"
|
||||
)
|
||||
self.handler_names = set()
|
||||
self.gcode_handlers = {}
|
||||
self.gcode_handlers_no_data = {}
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.gcode_handlers or item in self.gcode_handlers_no_data
|
||||
|
||||
def _get_required_args_count(self, func):
|
||||
sig = signature(func)
|
||||
required_count = sum(
|
||||
1
|
||||
for p in sig.parameters.values()
|
||||
if (p.kind == p.POSITIONAL_OR_KEYWORD or p.kind == p.POSITIONAL_ONLY)
|
||||
and p.default == p.empty
|
||||
)
|
||||
return required_count
|
||||
|
||||
def register(self, gcode):
|
||||
def decorator(func):
|
||||
required_count = self._get_required_args_count(func)
|
||||
if required_count == 1:
|
||||
self.gcode_handlers_no_data[gcode] = func
|
||||
elif required_count == 2:
|
||||
self.gcode_handlers[gcode] = func
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Cannot register function with {required_count} required parameters"
|
||||
)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def register_no_data(self, gcode):
|
||||
def decorator(func):
|
||||
self.gcode_handlers_no_data[gcode] = func
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def execute(self, printer, gcode, data):
|
||||
gcode_info = self._gcode_with_info(gcode)
|
||||
try:
|
||||
if gcode in self.gcode_handlers:
|
||||
self._log.debug(f"Executing {gcode_info}")
|
||||
return self.gcode_handlers[gcode](printer, data)
|
||||
elif gcode in self.gcode_handlers_no_data:
|
||||
self._log.debug(f"Executing {gcode_info}")
|
||||
return self.gcode_handlers_no_data[gcode](printer)
|
||||
else:
|
||||
self._log.debug(f"ignoring {gcode_info} command.")
|
||||
return False
|
||||
except Exception as e:
|
||||
self._log.error(f"Error during gcode {gcode_info}")
|
||||
raise
|
||||
|
||||
def _gcode_with_info(self, gcode):
|
||||
return f"{gcode} ({GCODE_DOCUMENTATION.get(gcode, 'Info not specified')})"
|
21
octoprint_bambu_printer/printer/print_job.py
Normal file
21
octoprint_bambu_printer/printer/print_job.py
Normal file
@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import (
|
||||
FileInfo,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrintJob:
|
||||
file_info: FileInfo
|
||||
progress: int
|
||||
remaining_time: int
|
||||
current_layer: int
|
||||
total_layers: int
|
||||
|
||||
@property
|
||||
def file_position(self):
|
||||
if self.file_info.size is None:
|
||||
return 0
|
||||
return int(self.file_info.size * self.progress / 100)
|
257
octoprint_bambu_printer/printer/printer_serial_io.py
Normal file
257
octoprint_bambu_printer/printer/printer_serial_io.py
Normal file
@ -0,0 +1,257 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from io import BufferedIOBase
|
||||
import logging
|
||||
import queue
|
||||
import re
|
||||
import threading
|
||||
import traceback
|
||||
from types import TracebackType
|
||||
from typing import Callable
|
||||
|
||||
from octoprint.util import to_bytes, to_unicode
|
||||
from serial import SerialTimeoutException
|
||||
|
||||
|
||||
class PrinterSerialIO(threading.Thread, BufferedIOBase):
|
||||
command_regex = re.compile(r"^([GM])(\d+)")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
handle_command_callback: Callable[[str, str], None],
|
||||
settings,
|
||||
serial_log_handler=None,
|
||||
read_timeout=5.0,
|
||||
write_timeout=10.0,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name="octoprint.plugins.bambu_printer.printer_worker", daemon=True
|
||||
)
|
||||
self._handle_command_callback = handle_command_callback
|
||||
self._settings = settings
|
||||
self._log = self._init_logger(serial_log_handler)
|
||||
|
||||
self._read_timeout = read_timeout
|
||||
self._write_timeout = write_timeout
|
||||
|
||||
self.current_line = 0
|
||||
self._received_lines = 0
|
||||
self._wait_interval = 5.0
|
||||
self._running = True
|
||||
|
||||
self._rx_buffer_size = 64
|
||||
self._incoming_lock = threading.RLock()
|
||||
|
||||
self.input_bytes = queue.Queue(self._rx_buffer_size)
|
||||
self.output_bytes = queue.Queue()
|
||||
self._error_detected: Exception | None = None
|
||||
|
||||
def _init_logger(self, log_handler):
|
||||
log = logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter.serial")
|
||||
if log_handler is not None:
|
||||
log.addHandler(log_handler)
|
||||
log.debug("-" * 78)
|
||||
return log
|
||||
|
||||
@property
|
||||
def incoming_lock(self):
|
||||
return self._incoming_lock
|
||||
|
||||
def run(self) -> None:
|
||||
buffer = b""
|
||||
|
||||
while self._running:
|
||||
try:
|
||||
data = self.input_bytes.get(block=True, timeout=0.01)
|
||||
data = to_bytes(data, encoding="ascii", errors="replace")
|
||||
|
||||
buffer += data
|
||||
line, buffer = self._read_next_line(buffer)
|
||||
while line is not None:
|
||||
self._received_lines += 1
|
||||
self._process_input_gcode_line(line)
|
||||
line, buffer = self._read_next_line(buffer)
|
||||
self.input_bytes.task_done()
|
||||
except queue.Empty:
|
||||
continue
|
||||
except Exception as e:
|
||||
self._error_detected = e
|
||||
self.input_bytes.task_done()
|
||||
self._clearQueue(self.input_bytes)
|
||||
self._log.info(
|
||||
"\n".join(traceback.format_exception_only(type(e), e)[-50:])
|
||||
)
|
||||
self._running = False
|
||||
|
||||
self._log.debug("Closing IO read loop")
|
||||
|
||||
def _read_next_line(self, buffer: bytes):
|
||||
new_line_pos = buffer.find(b"\n") + 1
|
||||
if new_line_pos > 0:
|
||||
line = buffer[:new_line_pos]
|
||||
buffer = buffer[new_line_pos:]
|
||||
return line, buffer
|
||||
else:
|
||||
return None, buffer
|
||||
|
||||
def close(self):
|
||||
self.flush()
|
||||
self._running = False
|
||||
self.join()
|
||||
|
||||
def flush(self):
|
||||
self.input_bytes.join()
|
||||
self.raise_if_error()
|
||||
|
||||
def raise_if_error(self):
|
||||
if self._error_detected is not None:
|
||||
raise self._error_detected
|
||||
|
||||
def write(self, data: bytes) -> int:
|
||||
data = to_bytes(data, errors="replace")
|
||||
u_data = to_unicode(data, errors="replace")
|
||||
|
||||
with self._incoming_lock:
|
||||
if self.is_closed():
|
||||
return 0
|
||||
|
||||
try:
|
||||
self._log.debug(f"<<< {u_data}")
|
||||
self.input_bytes.put(data, timeout=self._write_timeout)
|
||||
return len(data)
|
||||
except queue.Full:
|
||||
self._log.error(
|
||||
"Incoming queue is full, raising SerialTimeoutException"
|
||||
)
|
||||
raise SerialTimeoutException()
|
||||
|
||||
def readline(self) -> bytes:
|
||||
try:
|
||||
# fetch a line from the queue, wait no longer than timeout
|
||||
line = to_unicode(
|
||||
self.output_bytes.get(timeout=self._read_timeout), errors="replace"
|
||||
)
|
||||
self._log.debug(f">>> {line.strip()}")
|
||||
self.output_bytes.task_done()
|
||||
return to_bytes(line)
|
||||
except queue.Empty:
|
||||
# queue empty? return empty line
|
||||
return b""
|
||||
|
||||
def readlines(self):
|
||||
result = []
|
||||
next_line = self.readline()
|
||||
while next_line != b"":
|
||||
result.append(next_line)
|
||||
next_line = self.readline()
|
||||
return result
|
||||
|
||||
def send(self, line: str) -> None:
|
||||
if self.output_bytes is not None:
|
||||
self.output_bytes.put(line)
|
||||
|
||||
def sendOk(self):
|
||||
self.send("ok")
|
||||
|
||||
def reset(self):
|
||||
self._clearQueue(self.input_bytes)
|
||||
self._clearQueue(self.output_bytes)
|
||||
|
||||
def is_closed(self):
|
||||
return not self._running
|
||||
|
||||
def _process_input_gcode_line(self, data: bytes):
|
||||
if b"*" in data:
|
||||
checksum = int(data[data.rfind(b"*") + 1 :])
|
||||
data = data[: data.rfind(b"*")]
|
||||
if not checksum == self._calculate_checksum(data):
|
||||
self._triggerResend(expected=self.current_line + 1)
|
||||
return
|
||||
|
||||
self.current_line += 1
|
||||
elif self._settings.get_boolean(["forceChecksum"]):
|
||||
self.send(self._format_error("checksum_missing"))
|
||||
return
|
||||
|
||||
line = self._process_linenumber_marker(data)
|
||||
if line is None:
|
||||
return
|
||||
|
||||
command = to_unicode(line, encoding="ascii", errors="replace").strip()
|
||||
command_match = self.command_regex.match(command)
|
||||
if command_match is not None:
|
||||
gcode = command_match.group(0)
|
||||
self._handle_command_callback(gcode, command)
|
||||
else:
|
||||
self._log.warn(f'Not a valid gcode command "{command}"')
|
||||
|
||||
def _process_linenumber_marker(self, data: bytes):
|
||||
linenumber = 0
|
||||
if data.startswith(b"N") and b"M110" in data:
|
||||
linenumber = int(re.search(b"N([0-9]+)", data).group(1))
|
||||
self.lastN = linenumber
|
||||
self.current_line = linenumber
|
||||
self.sendOk()
|
||||
return None
|
||||
elif data.startswith(b"N"):
|
||||
linenumber = int(re.search(b"N([0-9]+)", data).group(1))
|
||||
expected = self.lastN + 1
|
||||
if linenumber != expected:
|
||||
self._triggerResend(actual=linenumber)
|
||||
return None
|
||||
else:
|
||||
self.lastN = linenumber
|
||||
data = data.split(None, 1)[1].strip()
|
||||
return data
|
||||
|
||||
def _triggerResend(
|
||||
self,
|
||||
expected: int | None = None,
|
||||
actual: int | None = None,
|
||||
checksum: int | None = None,
|
||||
) -> None:
|
||||
with self._incoming_lock:
|
||||
if expected is None:
|
||||
expected = self.lastN + 1
|
||||
else:
|
||||
self.lastN = expected - 1
|
||||
|
||||
if actual is None:
|
||||
if checksum:
|
||||
self.send(self._format_error("checksum_mismatch"))
|
||||
else:
|
||||
self.send(self._format_error("checksum_missing"))
|
||||
else:
|
||||
self.send(self._format_error("lineno_mismatch", expected, actual))
|
||||
|
||||
def request_resend():
|
||||
self.send("Resend:%d" % expected)
|
||||
self.sendOk()
|
||||
|
||||
request_resend()
|
||||
|
||||
def _calculate_checksum(self, line: bytes) -> int:
|
||||
checksum = 0
|
||||
for c in bytearray(line):
|
||||
checksum ^= c
|
||||
return checksum
|
||||
|
||||
def _format_error(self, error: str, *args, **kwargs) -> str:
|
||||
errors = {
|
||||
"checksum_mismatch": "Checksum mismatch",
|
||||
"checksum_missing": "Missing checksum",
|
||||
"lineno_mismatch": "expected line {} got {}",
|
||||
"lineno_missing": "No Line Number with checksum, Last Line: {}",
|
||||
"maxtemp": "MAXTEMP triggered!",
|
||||
"mintemp": "MINTEMP triggered!",
|
||||
"command_unknown": "Unknown command {}",
|
||||
}
|
||||
return f"Error: {errors.get(error).format(*args, **kwargs)}"
|
||||
|
||||
def _clearQueue(self, q: queue.Queue):
|
||||
try:
|
||||
while q.get(block=False):
|
||||
q.task_done()
|
||||
continue
|
||||
except queue.Empty:
|
||||
pass
|
4
octoprint_bambu_printer/printer/pybambu/__init__.py
Normal file
4
octoprint_bambu_printer/printer/pybambu/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
"""Initialise the Bambu Client"""
|
||||
# TODO: Once complete, move pybambu to PyPi
|
||||
from .bambu_client import BambuClient
|
||||
from .bambu_cloud import BambuCloud
|
594
octoprint_bambu_printer/printer/pybambu/bambu_client.py
Normal file
594
octoprint_bambu_printer/printer/pybambu/bambu_client.py
Normal file
@ -0,0 +1,594 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import queue
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import socket
|
||||
import ssl
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from .bambu_cloud import BambuCloud
|
||||
from .const import (
|
||||
LOGGER,
|
||||
Features,
|
||||
)
|
||||
from .models import Device, SlicerSettings
|
||||
from .commands import (
|
||||
GET_VERSION,
|
||||
PUSH_ALL,
|
||||
START_PUSH,
|
||||
)
|
||||
|
||||
|
||||
class WatchdogThread(threading.Thread):
|
||||
|
||||
def __init__(self, client):
|
||||
self._client = client
|
||||
self._watchdog_fired = False
|
||||
self._stop_event = threading.Event()
|
||||
self._last_received_data = time.time()
|
||||
super().__init__()
|
||||
self.daemon = True
|
||||
self.setName(f"{self._client._device.info.device_type}-Watchdog-{threading.get_native_id()}")
|
||||
|
||||
def stop(self):
|
||||
self._stop_event.set()
|
||||
|
||||
def received_data(self):
|
||||
self._last_received_data = time.time()
|
||||
|
||||
def run(self):
|
||||
LOGGER.info("Watchdog thread started.")
|
||||
WATCHDOG_TIMER = 30
|
||||
while True:
|
||||
# Wait out the remainder of the watchdog delay or 1s, whichever is higher.
|
||||
interval = time.time() - self._last_received_data
|
||||
wait_time = max(1, WATCHDOG_TIMER - interval)
|
||||
if self._stop_event.wait(wait_time):
|
||||
# Stop event has been set. Exit thread.
|
||||
break
|
||||
interval = time.time() - self._last_received_data
|
||||
if not self._watchdog_fired and (interval > WATCHDOG_TIMER):
|
||||
LOGGER.debug(f"Watchdog fired. No data received for {math.floor(interval)} seconds for {self._client._serial}.")
|
||||
self._watchdog_fired = True
|
||||
self._client._on_watchdog_fired()
|
||||
elif interval < WATCHDOG_TIMER:
|
||||
self._watchdog_fired = False
|
||||
|
||||
LOGGER.info("Watchdog thread exited.")
|
||||
|
||||
|
||||
class ChamberImageThread(threading.Thread):
|
||||
def __init__(self, client):
|
||||
self._client = client
|
||||
self._stop_event = threading.Event()
|
||||
super().__init__()
|
||||
self.daemon = True
|
||||
self.setName(f"{self._client._device.info.device_type}-Chamber-{threading.get_native_id()}")
|
||||
|
||||
def stop(self):
|
||||
self._stop_event.set()
|
||||
|
||||
def run(self):
|
||||
LOGGER.debug("Chamber image thread started.")
|
||||
|
||||
auth_data = bytearray()
|
||||
|
||||
username = 'bblp'
|
||||
access_code = self._client._access_code
|
||||
hostname = self._client.host
|
||||
port = 6000
|
||||
MAX_CONNECT_ATTEMPTS = 12
|
||||
connect_attempts = 0
|
||||
|
||||
auth_data += struct.pack("<I", 0x40) # '@'\0\0\0
|
||||
auth_data += struct.pack("<I", 0x3000) # \0'0'\0\0
|
||||
auth_data += struct.pack("<I", 0) # \0\0\0\0
|
||||
auth_data += struct.pack("<I", 0) # \0\0\0\0
|
||||
for i in range(0, len(username)):
|
||||
auth_data += struct.pack("<c", username[i].encode('ascii'))
|
||||
for i in range(0, 32 - len(username)):
|
||||
auth_data += struct.pack("<x")
|
||||
for i in range(0, len(access_code)):
|
||||
auth_data += struct.pack("<c", access_code[i].encode('ascii'))
|
||||
for i in range(0, 32 - len(access_code)):
|
||||
auth_data += struct.pack("<x")
|
||||
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
jpeg_start = bytearray([0xff, 0xd8, 0xff, 0xe0])
|
||||
jpeg_end = bytearray([0xff, 0xd9])
|
||||
|
||||
read_chunk_size = 4096 # 4096 is the max we'll get even if we increase this.
|
||||
|
||||
# Payload format for each image is:
|
||||
# 16 byte header:
|
||||
# Bytes 0:3 = little endian payload size for the jpeg image (does not include this header).
|
||||
# Bytes 4:7 = 0x00000000
|
||||
# Bytes 8:11 = 0x00000001
|
||||
# Bytes 12:15 = 0x00000000
|
||||
# These first 16 bytes are always delivered by themselves.
|
||||
#
|
||||
# Bytes 16:19 = jpeg_start magic bytes
|
||||
# Bytes 20:payload_size-2 = jpeg image bytes
|
||||
# Bytes payload_size-2:payload_size = jpeg_end magic bytes
|
||||
#
|
||||
# Further attempts to receive data will get SSLWantReadError until a new image is ready (1-2 seconds later)
|
||||
while connect_attempts < MAX_CONNECT_ATTEMPTS and not self._stop_event.is_set():
|
||||
connect_attempts += 1
|
||||
try:
|
||||
with socket.create_connection((hostname, port)) as sock:
|
||||
try:
|
||||
sslSock = ctx.wrap_socket(sock, server_hostname=hostname)
|
||||
sslSock.write(auth_data)
|
||||
img = None
|
||||
payload_size = 0
|
||||
|
||||
status = sslSock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
|
||||
LOGGER.debug(f"SOCKET STATUS: {status}")
|
||||
if status != 0:
|
||||
LOGGER.error(f"Socket error: {status}")
|
||||
except socket.error as e:
|
||||
LOGGER.error(f"Socket error: {e}")
|
||||
# Sleep to allow printer to stabilize during boot when it may fail these connection attempts repeatedly.
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
sslSock.setblocking(False)
|
||||
while not self._stop_event.is_set():
|
||||
try:
|
||||
dr = sslSock.recv(read_chunk_size)
|
||||
#LOGGER.debug(f"Received {len(dr)} bytes.")
|
||||
|
||||
except ssl.SSLWantReadError:
|
||||
#LOGGER.debug("SSLWantReadError")
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
LOGGER.error("A Chamber Image thread inner exception occurred:")
|
||||
LOGGER.error(f"Exception. Type: {type(e)} Args: {e}")
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
if img is not None and len(dr) > 0:
|
||||
img += dr
|
||||
if len(img) > payload_size:
|
||||
# We got more data than we expected.
|
||||
LOGGER.error(f"Unexpected image payload received: {len(img)} > {payload_size}")
|
||||
# Reset buffer
|
||||
img = None
|
||||
elif len(img) == payload_size:
|
||||
# We should have the full image now.
|
||||
if img[:4] != jpeg_start:
|
||||
LOGGER.error("JPEG start magic bytes missing.")
|
||||
elif img[-2:] != jpeg_end:
|
||||
LOGGER.error("JPEG end magic bytes missing.")
|
||||
else:
|
||||
# Content is as expected. Send it.
|
||||
self._client.on_jpeg_received(img)
|
||||
|
||||
# Reset buffer
|
||||
img = None
|
||||
# else:
|
||||
# Otherwise we need to continue looping without reseting the buffer to receive the remaining data
|
||||
# and without delaying.
|
||||
|
||||
elif len(dr) == 16:
|
||||
# We got the header bytes. Get the expected payload size from it and create the image buffer bytearray.
|
||||
# Reset connect_attempts now we know the connect was successful.
|
||||
connect_attempts = 0
|
||||
img = bytearray()
|
||||
payload_size = int.from_bytes(dr[0:3], byteorder='little')
|
||||
|
||||
elif len(dr) == 0:
|
||||
# This occurs if the wrong access code was provided.
|
||||
LOGGER.error("Chamber image connection rejected by the printer. Check provided access code and IP address.")
|
||||
# Sleep for a short while and then re-attempt the connection.
|
||||
time.sleep(5)
|
||||
break
|
||||
|
||||
else:
|
||||
LOGGER.error(f"UNEXPECTED DATA RECEIVED: {len(dr)}")
|
||||
time.sleep(1)
|
||||
|
||||
except OSError as e:
|
||||
if e.errno == 113:
|
||||
LOGGER.debug("Host is unreachable")
|
||||
else:
|
||||
LOGGER.error("A Chamber Image thread outer exception occurred:")
|
||||
LOGGER.error(f"Exception. Type: {type(e)} Args: {e}")
|
||||
if not self._stop_event.is_set():
|
||||
time.sleep(1) # Avoid a tight loop if this is a persistent error.
|
||||
|
||||
except Exception as e:
|
||||
LOGGER.error(f"A Chamber Image thread outer exception occurred:")
|
||||
LOGGER.error(f"Exception. Type: {type(e)} Args: {e}")
|
||||
if not self._stop_event.is_set():
|
||||
time.sleep(1) # Avoid a tight loop if this is a persistent error.
|
||||
|
||||
LOGGER.debug("Chamber image thread exited.")
|
||||
|
||||
|
||||
class MqttThread(threading.Thread):
|
||||
def __init__(self, client):
|
||||
self._client = client
|
||||
self._stop_event = threading.Event()
|
||||
super().__init__()
|
||||
self.daemon = True
|
||||
self.setName(f"{self._client._device.info.device_type}-Mqtt-{threading.get_native_id()}")
|
||||
|
||||
def stop(self):
|
||||
self._stop_event.set()
|
||||
|
||||
def run(self):
|
||||
LOGGER.info("MQTT listener thread started.")
|
||||
exceptionSeen = ""
|
||||
while True:
|
||||
try:
|
||||
host = self._client.host if self._client._local_mqtt else self._client.bambu_cloud.cloud_mqtt_host
|
||||
LOGGER.debug(f"Connect: Attempting Connection to {host}")
|
||||
self._client.client.connect(host, self._client._port, keepalive=5)
|
||||
|
||||
LOGGER.debug("Starting listen loop")
|
||||
self._client.client.loop_forever()
|
||||
LOGGER.debug("Ended listen loop.")
|
||||
break
|
||||
except TimeoutError as e:
|
||||
if exceptionSeen != "TimeoutError":
|
||||
LOGGER.debug(f"TimeoutError: {e}.")
|
||||
exceptionSeen = "TimeoutError"
|
||||
time.sleep(5)
|
||||
except ConnectionError as e:
|
||||
if exceptionSeen != "ConnectionError":
|
||||
LOGGER.debug(f"ConnectionError: {e}.")
|
||||
exceptionSeen = "ConnectionError"
|
||||
time.sleep(5)
|
||||
except OSError as e:
|
||||
if e.errno == 113:
|
||||
if exceptionSeen != "OSError113":
|
||||
LOGGER.debug(f"OSError: {e}.")
|
||||
exceptionSeen = "OSError113"
|
||||
time.sleep(5)
|
||||
else:
|
||||
LOGGER.error("A listener loop thread exception occurred:")
|
||||
LOGGER.error(f"Exception. Type: {type(e)} Args: {e}")
|
||||
time.sleep(1) # Avoid a tight loop if this is a persistent error.
|
||||
except Exception as e:
|
||||
LOGGER.error("A listener loop thread exception occurred:")
|
||||
LOGGER.error(f"Exception. Type: {type(e)} Args: {e}")
|
||||
time.sleep(1) # Avoid a tight loop if this is a persistent error.
|
||||
|
||||
if self._client.client is None:
|
||||
break
|
||||
|
||||
self._client.client.disconnect()
|
||||
|
||||
LOGGER.info("MQTT listener thread exited.")
|
||||
|
||||
|
||||
@dataclass
|
||||
class BambuClient:
|
||||
"""Initialize Bambu Client to connect to MQTT Broker"""
|
||||
_watchdog = None
|
||||
_camera = None
|
||||
_usage_hours: float
|
||||
|
||||
def __init__(self, config):
|
||||
self.host = config['host']
|
||||
self._callback = None
|
||||
|
||||
self._access_code = config.get('access_code', '')
|
||||
self._auth_token = config.get('auth_token', '')
|
||||
self._device_type = config.get('device_type', 'unknown')
|
||||
self._local_mqtt = config.get('local_mqtt', False)
|
||||
self._manual_refresh_mode = config.get('manual_refresh_mode', False)
|
||||
self._serial = config.get('serial', '')
|
||||
self._usage_hours = config.get('usage_hours', 0)
|
||||
self._username = config.get('username', '')
|
||||
self._enable_camera = config.get('enable_camera', True)
|
||||
|
||||
self._connected = False
|
||||
self._port = 1883
|
||||
self._refreshed = False
|
||||
|
||||
self._device = Device(self)
|
||||
self.bambu_cloud = BambuCloud(
|
||||
config.get('region', ''),
|
||||
config.get('email', ''),
|
||||
config.get('username', ''),
|
||||
config.get('auth_token', '')
|
||||
)
|
||||
self.slicer_settings = SlicerSettings(self)
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Return if connected to server"""
|
||||
return self._connected
|
||||
|
||||
@property
|
||||
def manual_refresh_mode(self):
|
||||
"""Return if the integration is running in poll mode"""
|
||||
return self._manual_refresh_mode
|
||||
|
||||
async def set_manual_refresh_mode(self, on):
|
||||
self._manual_refresh_mode = on
|
||||
if self._manual_refresh_mode:
|
||||
# Disconnect from the server. User must manually hit the refresh button to connect to refresh and then it will immediately disconnect.
|
||||
self.disconnect()
|
||||
else:
|
||||
# Reconnect normally
|
||||
self.connect(self._callback)
|
||||
|
||||
@property
|
||||
def camera_enabled(self):
|
||||
return self._enable_camera
|
||||
|
||||
def callback(self, event: str):
|
||||
if self._callback is not None:
|
||||
self._callback(event)
|
||||
|
||||
def set_camera_enabled(self, enable):
|
||||
self._enable_camera = enable
|
||||
if self._enable_camera:
|
||||
self._start_camera()
|
||||
else:
|
||||
self._stop_camera()
|
||||
|
||||
def setup_tls(self):
|
||||
self.client.tls_set(tls_version=ssl.PROTOCOL_TLS, cert_reqs=ssl.CERT_NONE)
|
||||
self.client.tls_insecure_set(True)
|
||||
|
||||
def connect(self, callback):
|
||||
"""Connect to the MQTT Broker"""
|
||||
self.client = mqtt.Client()
|
||||
self._callback = callback
|
||||
self.client.on_connect = self.on_connect
|
||||
self.client.on_disconnect = self.on_disconnect
|
||||
self.client.on_message = self.on_message
|
||||
# Set aggressive reconnect polling.
|
||||
self.client.reconnect_delay_set(min_delay=1, max_delay=1)
|
||||
|
||||
# Run the blocking tls_set method in a separate thread
|
||||
self.setup_tls()
|
||||
|
||||
self._port = 8883
|
||||
if self._local_mqtt:
|
||||
self.client.username_pw_set("bblp", password=self._access_code)
|
||||
else:
|
||||
self.client.username_pw_set(self._username, password=self._auth_token)
|
||||
|
||||
LOGGER.debug("Starting MQTT listener thread")
|
||||
self._mqtt = MqttThread(self)
|
||||
self._mqtt.start()
|
||||
|
||||
def subscribe_and_request_info(self):
|
||||
LOGGER.debug("Loading slicer settings...")
|
||||
self.slicer_settings.update()
|
||||
LOGGER.debug("Now subscribing...")
|
||||
self.subscribe()
|
||||
LOGGER.debug("On Connect: Getting version info")
|
||||
self.publish(GET_VERSION)
|
||||
LOGGER.debug("On Connect: Request push all")
|
||||
self.publish(PUSH_ALL)
|
||||
|
||||
def on_connect(self,
|
||||
client_: mqtt.Client,
|
||||
userdata: None,
|
||||
flags: dict[str, Any],
|
||||
result_code: int,
|
||||
properties: mqtt.Properties | None = None, ):
|
||||
"""Handle connection"""
|
||||
LOGGER.info("On Connect: Connected to printer")
|
||||
self._on_connect()
|
||||
|
||||
def _start_camera(self):
|
||||
if not self._device.supports_feature(Features.CAMERA_RTSP):
|
||||
if self._device.supports_feature(Features.CAMERA_IMAGE):
|
||||
if self._enable_camera:
|
||||
LOGGER.debug("Starting Chamber Image thread")
|
||||
self._camera = ChamberImageThread(self)
|
||||
self._camera.start()
|
||||
elif (self.host == "") or (self._access_code == ""):
|
||||
LOGGER.debug("Skipping camera setup as local access details not provided.")
|
||||
|
||||
def _stop_camera(self):
|
||||
if self._camera is not None:
|
||||
LOGGER.debug("Stopping camera thread")
|
||||
self._camera.stop()
|
||||
self._camera.join()
|
||||
|
||||
def _on_connect(self):
|
||||
self._connected = True
|
||||
self.subscribe_and_request_info()
|
||||
|
||||
LOGGER.debug("Starting watchdog thread")
|
||||
self._watchdog = WatchdogThread(self)
|
||||
self._watchdog.start()
|
||||
|
||||
self._start_camera()
|
||||
|
||||
def try_on_connect(self,
|
||||
client_: mqtt.Client,
|
||||
userdata: None,
|
||||
flags: dict[str, Any],
|
||||
result_code: int,
|
||||
properties: mqtt.Properties | None = None, ):
|
||||
"""Handle connection"""
|
||||
LOGGER.info("On Connect: Connected to printer")
|
||||
self._connected = True
|
||||
LOGGER.debug("Now test subscribing...")
|
||||
self.subscribe()
|
||||
# For the initial configuration connection attempt, we just need version info.
|
||||
LOGGER.debug("On Connect: Getting version info")
|
||||
self.publish(GET_VERSION)
|
||||
|
||||
def on_disconnect(self,
|
||||
client_: mqtt.Client,
|
||||
userdata: None,
|
||||
result_code: int):
|
||||
"""Called when MQTT Disconnects"""
|
||||
LOGGER.warn(f"On Disconnect: Printer disconnected with error code: {result_code}")
|
||||
self._on_disconnect()
|
||||
|
||||
def _on_disconnect(self):
|
||||
LOGGER.debug("_on_disconnect: Lost connection to the printer")
|
||||
self._connected = False
|
||||
self._device.info.set_online(False)
|
||||
if self._watchdog is not None:
|
||||
LOGGER.debug("Stopping watchdog thread")
|
||||
self._watchdog.stop()
|
||||
self._watchdog.join()
|
||||
self._stop_camera()
|
||||
|
||||
def _on_watchdog_fired(self):
|
||||
LOGGER.info("Watch dog fired")
|
||||
self._device.info.set_online(False)
|
||||
self.publish(START_PUSH)
|
||||
|
||||
def on_jpeg_received(self, bytes):
|
||||
self._device.chamber_image.set_jpeg(bytes)
|
||||
|
||||
def on_message(self, client, userdata, message):
|
||||
"""Return the payload when received"""
|
||||
try:
|
||||
# X1 mqtt payload is inconsistent. Adjust it for consistent logging.
|
||||
clean_msg = re.sub(r"\\n *", "", str(message.payload))
|
||||
if self._refreshed:
|
||||
LOGGER.debug(f"Received data: {clean_msg}")
|
||||
|
||||
json_data = json.loads(message.payload)
|
||||
if json_data.get("event"):
|
||||
# These are events from the bambu cloud mqtt feed and allow us to detect when a local
|
||||
# device has connected/disconnected (e.g. turned on/off)
|
||||
if json_data.get("event").get("event") == "client.connected":
|
||||
LOGGER.debug("Client connected event received.")
|
||||
self._on_disconnect() # We aren't guaranteed to recieve a client.disconnected event.
|
||||
self._on_connect()
|
||||
elif json_data.get("event").get("event") == "client.disconnected":
|
||||
LOGGER.debug("Client disconnected event received.")
|
||||
self._on_disconnect()
|
||||
else:
|
||||
self._device.info.set_online(True)
|
||||
self._watchdog.received_data()
|
||||
if json_data.get("print"):
|
||||
self._device.print_update(data=json_data.get("print"))
|
||||
# Once we receive data, if in manual refresh mode, we disconnect again.
|
||||
if self._manual_refresh_mode:
|
||||
self.disconnect()
|
||||
if json_data.get("print").get("msg", 0) == 0:
|
||||
self._refreshed= False
|
||||
elif json_data.get("info") and json_data.get("info").get("command") == "get_version":
|
||||
LOGGER.debug("Got Version Data")
|
||||
self._device.info_update(data=json_data.get("info"))
|
||||
except Exception as e:
|
||||
LOGGER.error("An exception occurred processing a message:", exc_info=e)
|
||||
|
||||
def subscribe(self):
|
||||
"""Subscribe to report topic"""
|
||||
LOGGER.debug(f"Subscribing: device/{self._serial}/report")
|
||||
self.client.subscribe(f"device/{self._serial}/report")
|
||||
|
||||
def publish(self, msg):
|
||||
"""Publish a custom message"""
|
||||
result = self.client.publish(f"device/{self._serial}/request", json.dumps(msg))
|
||||
status = result[0]
|
||||
if status == 0:
|
||||
LOGGER.debug(f"Sent {msg} to topic device/{self._serial}/request")
|
||||
return True
|
||||
|
||||
LOGGER.error(f"Failed to send message to topic device/{self._serial}/request")
|
||||
return False
|
||||
|
||||
async def refresh(self):
|
||||
"""Force refresh data"""
|
||||
|
||||
if self._manual_refresh_mode:
|
||||
self.connect(self._callback)
|
||||
else:
|
||||
LOGGER.debug("Force Refresh: Getting Version Info")
|
||||
self._refreshed = True
|
||||
self.publish(GET_VERSION)
|
||||
LOGGER.debug("Force Refresh: Request Push All")
|
||||
self._refreshed = True
|
||||
self.publish(PUSH_ALL)
|
||||
|
||||
self.slicer_settings.update()
|
||||
|
||||
def get_device(self):
|
||||
"""Return device"""
|
||||
return self._device
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect the Bambu Client from server"""
|
||||
LOGGER.debug(" Disconnect: Client Disconnecting")
|
||||
if self.client is not None:
|
||||
self.client.disconnect()
|
||||
self.client = None
|
||||
|
||||
async def try_connection(self):
|
||||
"""Test if we can connect to an MQTT broker."""
|
||||
LOGGER.debug("Try Connection")
|
||||
|
||||
result: queue.Queue[bool] = queue.Queue(maxsize=1)
|
||||
|
||||
def on_message(client, userdata, message):
|
||||
json_data = json.loads(message.payload)
|
||||
LOGGER.debug(f"Try Connection: Got '{json_data}'")
|
||||
if json_data.get("info") and json_data.get("info").get("command") == "get_version":
|
||||
LOGGER.debug("Got Version Command Data")
|
||||
self._device.info_update(data=json_data.get("info"))
|
||||
result.put(True)
|
||||
|
||||
self.client = mqtt.Client()
|
||||
self.client.on_connect = self.try_on_connect
|
||||
self.client.on_disconnect = self.on_disconnect
|
||||
self.client.on_message = on_message
|
||||
|
||||
# Run the blocking tls_set method in a separate thread
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(None, self.setup_tls)
|
||||
|
||||
if self._local_mqtt:
|
||||
self.client.username_pw_set("bblp", password=self._access_code)
|
||||
else:
|
||||
self.client.username_pw_set(self._username, password=self._auth_token)
|
||||
self._port = 8883
|
||||
|
||||
LOGGER.debug("Test connection: Connecting to %s", self.host)
|
||||
try:
|
||||
self.client.connect(self.host, self._port)
|
||||
self.client.loop_start()
|
||||
if result.get(timeout=10):
|
||||
return True
|
||||
except OSError as e:
|
||||
return False
|
||||
except queue.Empty:
|
||||
return False
|
||||
finally:
|
||||
self.disconnect()
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Async enter.
|
||||
Returns:
|
||||
The BambuLab object.
|
||||
"""
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_exc_info):
|
||||
"""Async exit.
|
||||
Args:
|
||||
_exc_info: Exec type.
|
||||
"""
|
||||
self.disconnect()
|
598
octoprint_bambu_printer/printer/pybambu/bambu_cloud.py
Normal file
598
octoprint_bambu_printer/printer/pybambu/bambu_cloud.py
Normal file
@ -0,0 +1,598 @@
|
||||
from __future__ import annotations
|
||||
from enum import (
|
||||
Enum,
|
||||
)
|
||||
|
||||
import base64
|
||||
import cloudscraper
|
||||
import json
|
||||
import requests
|
||||
|
||||
class ConnectionMechanismEnum(Enum):
|
||||
CLOUDSCRAPER = 1,
|
||||
CURL_CFFI = 2,
|
||||
REQUESTS = 3
|
||||
|
||||
CONNECTION_MECHANISM = ConnectionMechanismEnum.CLOUDSCRAPER
|
||||
|
||||
curl_available = False
|
||||
if CONNECTION_MECHANISM == ConnectionMechanismEnum.CURL_CFFI:
|
||||
try:
|
||||
from curl_cffi import requests as curl_requests
|
||||
curl_available = True
|
||||
except ImportError:
|
||||
curl_available = False
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .const import (
|
||||
LOGGER,
|
||||
BambuUrl
|
||||
)
|
||||
|
||||
from .utils import get_Url
|
||||
|
||||
IMPERSONATE_BROWSER='chrome'
|
||||
|
||||
class CloudflareError(Exception):
|
||||
def __init__(self):
|
||||
super().__init__("Blocked by Cloudflare")
|
||||
self.error_code = 403
|
||||
|
||||
class EmailCodeRequiredError(Exception):
|
||||
def __init__(self):
|
||||
super().__init__("Email code required")
|
||||
self.error_code = 400
|
||||
|
||||
class EmailCodeExpiredError(Exception):
|
||||
def __init__(self):
|
||||
super().__init__("Email code expired")
|
||||
self.error_code = 400
|
||||
|
||||
class EmailCodeIncorrectError(Exception):
|
||||
def __init__(self):
|
||||
super().__init__("Email code incorrect")
|
||||
self.error_code = 400
|
||||
|
||||
class TfaCodeRequiredError(Exception):
|
||||
def __init__(self):
|
||||
super().__init__("Two factor authentication code required")
|
||||
self.error_code = 400
|
||||
|
||||
class CurlUnavailableError(Exception):
|
||||
def __init__(self):
|
||||
super().__init__("curl library unavailable")
|
||||
self.error_code = 400
|
||||
|
||||
@dataclass
|
||||
class BambuCloud:
|
||||
|
||||
def __init__(self, region: str, email: str, username: str, auth_token: str):
|
||||
self._region = region
|
||||
self._email = email
|
||||
self._username = username
|
||||
self._auth_token = auth_token
|
||||
self._tfaKey = None
|
||||
|
||||
def _get_headers(self):
|
||||
return {
|
||||
'User-Agent': 'bambu_network_agent/01.09.05.01',
|
||||
'X-BBL-Client-Name': 'OrcaSlicer',
|
||||
'X-BBL-Client-Type': 'slicer',
|
||||
'X-BBL-Client-Version': '01.09.05.51',
|
||||
'X-BBL-Language': 'en-US',
|
||||
'X-BBL-OS-Type': 'linux',
|
||||
'X-BBL-OS-Version': '6.2.0',
|
||||
'X-BBL-Agent-Version': '01.09.05.01',
|
||||
'X-BBL-Executable-info': '{}',
|
||||
'X-BBL-Agent-OS-Type': 'linux',
|
||||
'accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
# Orca/Bambu Studio also add this - need to work out what an appropriate ID is to put here:
|
||||
# 'X-BBL-Device-ID': BBL_AUTH_UUID,
|
||||
# Example: X-BBL-Device-ID: 370f9f43-c6fe-47d7-aec9-5fe5ef7e7673
|
||||
|
||||
def _get_headers_with_auth_token(self) -> dict:
|
||||
if CONNECTION_MECHANISM == ConnectionMechanismEnum.CURL_CFFI:
|
||||
headers = {}
|
||||
else:
|
||||
headers = self._get_headers()
|
||||
headers['Authorization'] = f"Bearer {self._auth_token}"
|
||||
return headers
|
||||
|
||||
def _test_response(self, response, return400=False):
|
||||
# Check specifically for cloudflare block
|
||||
if response.status_code == 403 and 'cloudflare' in response.text:
|
||||
LOGGER.debug("BLOCKED BY CLOUDFLARE")
|
||||
raise CloudflareError()
|
||||
|
||||
if response.status_code == 400 and not return400:
|
||||
LOGGER.error(f"Connection failed with error code: {response.status_code}")
|
||||
LOGGER.debug(f"Response: '{response.text}'")
|
||||
raise PermissionError(response.status_code, response.text)
|
||||
|
||||
if response.status_code > 400:
|
||||
LOGGER.error(f"Connection failed with error code: {response.status_code}")
|
||||
LOGGER.debug(f"Response: '{response.text}'")
|
||||
raise PermissionError(response.status_code, response.text)
|
||||
|
||||
LOGGER.debug(f"Response: {response.status_code}")
|
||||
|
||||
def _get(self, urlenum: BambuUrl):
|
||||
url = get_Url(urlenum, self._region)
|
||||
headers=self._get_headers_with_auth_token()
|
||||
if CONNECTION_MECHANISM == ConnectionMechanismEnum.CURL_CFFI:
|
||||
if not curl_available:
|
||||
LOGGER.debug(f"Curl library is unavailable.")
|
||||
raise CurlUnavailableError()
|
||||
response = curl_requests.get(url, headers=headers, timeout=10, impersonate=IMPERSONATE_BROWSER)
|
||||
elif CONNECTION_MECHANISM == ConnectionMechanismEnum.CLOUDSCRAPER:
|
||||
if len(headers) == 0:
|
||||
headers = self._get_headers()
|
||||
scraper = cloudscraper.create_scraper()
|
||||
response = scraper.get(url, headers=headers, timeout=10)
|
||||
elif CONNECTION_MECHANISM == ConnectionMechanismEnum.REQUESTS:
|
||||
if len(headers) == 0:
|
||||
headers = self._get_headers()
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
self._test_response(response)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def _post(self, urlenum: BambuUrl, json: str, headers={}, return400=False):
|
||||
url = get_Url(urlenum, self._region)
|
||||
if CONNECTION_MECHANISM == ConnectionMechanismEnum.CURL_CFFI:
|
||||
if not curl_available:
|
||||
LOGGER.debug(f"Curl library is unavailable.")
|
||||
raise CurlUnavailableError()
|
||||
response = curl_requests.post(url, headers=headers, json=json, impersonate=IMPERSONATE_BROWSER)
|
||||
elif CONNECTION_MECHANISM == ConnectionMechanismEnum.CLOUDSCRAPER:
|
||||
if len(headers) == 0:
|
||||
headers = self._get_headers()
|
||||
scraper = cloudscraper.create_scraper()
|
||||
response = scraper.post(url, headers=headers, json=json)
|
||||
elif CONNECTION_MECHANISM == ConnectionMechanismEnum.REQUESTS:
|
||||
if len(headers) == 0:
|
||||
headers = self._get_headers()
|
||||
response = requests.post(url, headers=headers, json=json)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
self._test_response(response, return400)
|
||||
|
||||
return response
|
||||
|
||||
def _get_authentication_token(self) -> str:
|
||||
LOGGER.debug("Getting accessToken from Bambu Cloud")
|
||||
|
||||
# First we need to find out how Bambu wants us to login.
|
||||
data = {
|
||||
"account": self._email,
|
||||
"password": self._password,
|
||||
"apiError": ""
|
||||
}
|
||||
|
||||
response = self._post(BambuUrl.LOGIN, json=data)
|
||||
|
||||
auth_json = response.json()
|
||||
accessToken = auth_json.get('accessToken', '')
|
||||
if accessToken != '':
|
||||
# We were provided the accessToken directly.
|
||||
return accessToken
|
||||
|
||||
loginType = auth_json.get("loginType", None)
|
||||
if loginType is None:
|
||||
LOGGER.error(f"loginType not present")
|
||||
LOGGER.error(f"Response not understood: '{response.text}'")
|
||||
return ValueError(0) # FIXME
|
||||
elif loginType == 'verifyCode':
|
||||
LOGGER.debug(f"Received verifyCode response")
|
||||
# raise EmailCodeRequiredError()
|
||||
return loginType
|
||||
elif loginType == 'tfa':
|
||||
# Store the tfaKey for later use
|
||||
LOGGER.debug(f"Received tfa response")
|
||||
self._tfaKey = auth_json.get("tfaKey")
|
||||
# raise TfaCodeRequiredError()
|
||||
return loginType
|
||||
else:
|
||||
LOGGER.debug(f"Did not understand json. loginType = '{loginType}'")
|
||||
LOGGER.error(f"Response not understood: '{response.text}'")
|
||||
return ValueError(1) # FIXME
|
||||
|
||||
def _get_email_verification_code(self):
|
||||
# Send the verification code request
|
||||
data = {
|
||||
"email": self._email,
|
||||
"type": "codeLogin"
|
||||
}
|
||||
|
||||
LOGGER.debug("Requesting verification code")
|
||||
self._post(BambuUrl.EMAIL_CODE, json=data)
|
||||
LOGGER.debug("Verification code requested successfully.")
|
||||
|
||||
def _get_authentication_token_with_verification_code(self, code) -> dict:
|
||||
LOGGER.debug("Attempting to connect with provided verification code.")
|
||||
data = {
|
||||
"account": self._email,
|
||||
"code": code
|
||||
}
|
||||
|
||||
response = self._post(BambuUrl.LOGIN, json=data, return400=True)
|
||||
status_code = response.status_code
|
||||
|
||||
if status_code == 200:
|
||||
LOGGER.debug("Authentication successful.")
|
||||
LOGGER.debug(f"Response = '{response.json()}'")
|
||||
elif status_code == 400:
|
||||
LOGGER.debug(f"Received response: {response.json()}")
|
||||
if response.json()['code'] == 1:
|
||||
# Code has expired. Request a new one.
|
||||
self._get_email_verification_code()
|
||||
raise EmailCodeExpiredError()
|
||||
elif response.json()['code'] == 2:
|
||||
# Code was incorrect. Let the user try again.
|
||||
raise EmailCodeIncorrectError()
|
||||
else:
|
||||
LOGGER.error(f"Response not understood: '{response.json()}'")
|
||||
raise ValueError(response.json()['code'])
|
||||
|
||||
return response.json()['accessToken']
|
||||
|
||||
def _get_authentication_token_with_2fa_code(self, code: str) -> dict:
|
||||
LOGGER.debug("Attempting to connect with provided 2FA code.")
|
||||
|
||||
data = {
|
||||
"tfaKey": self._tfaKey,
|
||||
"tfaCode": code
|
||||
}
|
||||
|
||||
response = self._post(BambuUrl.TFA_LOGIN, json=data)
|
||||
|
||||
LOGGER.debug(f"Response: {response.status_code}")
|
||||
if response.status_code == 200:
|
||||
LOGGER.debug("Authentication successful.")
|
||||
|
||||
cookies = response.cookies.get_dict()
|
||||
token_from_tfa = cookies.get("token")
|
||||
#LOGGER.debug(f"token_from_tfa: {token_from_tfa}")
|
||||
|
||||
return token_from_tfa
|
||||
|
||||
def _get_username_from_authentication_token(self) -> str:
|
||||
LOGGER.debug("Trying to get username from authentication token.")
|
||||
# User name is in 2nd portion of the auth token (delimited with periods)
|
||||
username = None
|
||||
tokens = self._auth_token.split(".")
|
||||
if len(tokens) != 3:
|
||||
LOGGER.debug("Received authToken is not a JWT.")
|
||||
LOGGER.debug("Trying to use project API to retrieve username instead")
|
||||
response = self.get_projects();
|
||||
if response is not None:
|
||||
projectsnode = response.get('projects', None)
|
||||
if projectsnode is None:
|
||||
LOGGER.debug("Failed to find projects node")
|
||||
else:
|
||||
if len(projectsnode) == 0:
|
||||
LOGGER.debug("No projects node in response")
|
||||
else:
|
||||
project=projectsnode[0]
|
||||
if project.get('user_id', None) is None:
|
||||
LOGGER.debug("No user_id entry")
|
||||
else:
|
||||
username = f"u_{project['user_id']}"
|
||||
LOGGER.debug(f"Found user_id of {username}")
|
||||
else:
|
||||
LOGGER.debug("Authentication token looks to be a JWT")
|
||||
try:
|
||||
b64_string = self._auth_token.split(".")[1]
|
||||
# String must be multiples of 4 chars in length. For decode pad with = character
|
||||
b64_string += "=" * ((4 - len(b64_string) % 4) % 4)
|
||||
jsonAuthToken = json.loads(base64.b64decode(b64_string))
|
||||
# Gives json payload with "username":"u_<digits>" within it
|
||||
username = jsonAuthToken.get('username', None)
|
||||
except:
|
||||
LOGGER.debug("Unable to decode authToken to json to retrieve username.")
|
||||
|
||||
if username is None:
|
||||
LOGGER.debug(f"Unable to decode authToken to retrieve username. AuthToken = {self._auth_token}")
|
||||
|
||||
return username
|
||||
|
||||
# Retrieves json description of devices in the form:
|
||||
# {
|
||||
# 'message': 'success',
|
||||
# 'code': None,
|
||||
# 'error': None,
|
||||
# 'devices': [
|
||||
# {
|
||||
# 'dev_id': 'REDACTED',
|
||||
# 'name': 'Bambu P1S',
|
||||
# 'online': True,
|
||||
# 'print_status': 'SUCCESS',
|
||||
# 'dev_model_name': 'C12',
|
||||
# 'dev_product_name': 'P1S',
|
||||
# 'dev_access_code': 'REDACTED',
|
||||
# 'nozzle_diameter': 0.4
|
||||
# },
|
||||
# {
|
||||
# 'dev_id': 'REDACTED',
|
||||
# 'name': 'Bambu P1P',
|
||||
# 'online': True,
|
||||
# 'print_status': 'RUNNING',
|
||||
# 'dev_model_name': 'C11',
|
||||
# 'dev_product_name': 'P1P',
|
||||
# 'dev_access_code': 'REDACTED',
|
||||
# 'nozzle_diameter': 0.4
|
||||
# },
|
||||
# {
|
||||
# 'dev_id': 'REDACTED',
|
||||
# 'name': 'Bambu X1C',
|
||||
# 'online': True,
|
||||
# 'print_status': 'RUNNING',
|
||||
# 'dev_model_name': 'BL-P001',
|
||||
# 'dev_product_name': 'X1 Carbon',
|
||||
# 'dev_access_code': 'REDACTED',
|
||||
# 'nozzle_diameter': 0.4
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
|
||||
def test_authentication(self, region: str, email: str, username: str, auth_token: str) -> bool:
|
||||
self._region = region
|
||||
self._email = email
|
||||
self._username = username
|
||||
self._auth_token = auth_token
|
||||
try:
|
||||
self.get_device_list()
|
||||
except:
|
||||
return False
|
||||
return True
|
||||
|
||||
def login(self, region: str, email: str, password: str) -> str:
|
||||
self._region = region
|
||||
self._email = email
|
||||
self._password = password
|
||||
|
||||
result = self._get_authentication_token()
|
||||
if result is None:
|
||||
LOGGER.error("Unable to authenticate.")
|
||||
return None
|
||||
elif len(result) < 20:
|
||||
return result
|
||||
else:
|
||||
self._auth_token = result
|
||||
self._username = self._get_username_from_authentication_token()
|
||||
return 'success'
|
||||
# self._auth_token = result
|
||||
# self._username = self._get_username_from_authentication_token()
|
||||
|
||||
def login_with_verification_code(self, code: str):
|
||||
result = self._get_authentication_token_with_verification_code(code)
|
||||
self._auth_token = result
|
||||
self._username = self._get_username_from_authentication_token()
|
||||
if self._auth_token != "" and self._username != "" and self._auth_token != None and self._username != None:
|
||||
return "success"
|
||||
|
||||
def login_with_2fa_code(self, code: str):
|
||||
result = self._get_authentication_token_with_2fa_code(code)
|
||||
self._auth_token = result
|
||||
self._username = self._get_username_from_authentication_token()
|
||||
if self._auth_token != "" and self._username != "" and self._auth_token != None and self._username != None:
|
||||
return "success"
|
||||
|
||||
def get_device_list(self) -> dict:
|
||||
LOGGER.debug("Getting device list from Bambu Cloud")
|
||||
try:
|
||||
response = self._get(BambuUrl.BIND)
|
||||
except:
|
||||
return None
|
||||
return response.json()['devices']
|
||||
|
||||
# The slicer settings are of the following form:
|
||||
#
|
||||
# {
|
||||
# "message": "success",
|
||||
# "code": null,
|
||||
# "error": null,
|
||||
# "print": {
|
||||
# "public": [
|
||||
# {
|
||||
# "setting_id": "GP004",
|
||||
# "version": "01.09.00.15",
|
||||
# "name": "0.20mm Standard @BBL X1C",
|
||||
# "update_time": "2024-07-04 11:27:08",
|
||||
# "nickname": null
|
||||
# },
|
||||
# ...
|
||||
# }
|
||||
# "private": []
|
||||
# },
|
||||
# "printer": {
|
||||
# "public": [
|
||||
# {
|
||||
# "setting_id": "GM001",
|
||||
# "version": "01.09.00.15",
|
||||
# "name": "Bambu Lab X1 Carbon 0.4 nozzle",
|
||||
# "update_time": "2024-07-04 11:25:07",
|
||||
# "nickname": null
|
||||
# },
|
||||
# ...
|
||||
# ],
|
||||
# "private": []
|
||||
# },
|
||||
# "filament": {
|
||||
# "public": [
|
||||
# {
|
||||
# "setting_id": "GFSA01",
|
||||
# "version": "01.09.00.15",
|
||||
# "name": "Bambu PLA Matte @BBL X1C",
|
||||
# "update_time": "2024-07-04 11:29:21",
|
||||
# "nickname": null,
|
||||
# "filament_id": "GFA01"
|
||||
# },
|
||||
# ...
|
||||
# ],
|
||||
# "private": [
|
||||
# {
|
||||
# "setting_id": "PFUS46ea5c221cabe5",
|
||||
# "version": "1.9.0.14",
|
||||
# "name": "Fillamentum PLA Extrafill @Bambu Lab X1 Carbon 0.4 nozzle",
|
||||
# "update_time": "2024-07-10 06:48:17",
|
||||
# "base_id": null,
|
||||
# "filament_id": "Pc628b24",
|
||||
# "filament_type": "PLA",
|
||||
# "filament_is_support": "0",
|
||||
# "nozzle_temperature": [
|
||||
# 190,
|
||||
# 240
|
||||
# ],
|
||||
# "nozzle_hrc": "3",
|
||||
# "filament_vendor": "Fillamentum"
|
||||
# },
|
||||
# ...
|
||||
# ]
|
||||
# },
|
||||
# "settings": {}
|
||||
# }
|
||||
|
||||
def get_slicer_settings(self) -> dict:
|
||||
LOGGER.debug("Getting slicer settings from Bambu Cloud")
|
||||
try:
|
||||
response = self._get(BambuUrl.SLICER_SETTINGS)
|
||||
except:
|
||||
return None
|
||||
LOGGER.debug("Succeeded")
|
||||
return response.json()
|
||||
|
||||
# The task list is of the following form with a 'hits' array with typical 20 entries.
|
||||
#
|
||||
# "total": 531,
|
||||
# "hits": [
|
||||
# {
|
||||
# "id": 35237965,
|
||||
# "designId": 0,
|
||||
# "designTitle": "",
|
||||
# "instanceId": 0,
|
||||
# "modelId": "REDACTED",
|
||||
# "title": "REDACTED",
|
||||
# "cover": "REDACTED",
|
||||
# "status": 4,
|
||||
# "feedbackStatus": 0,
|
||||
# "startTime": "2023-12-21T19:02:16Z",
|
||||
# "endTime": "2023-12-21T19:02:35Z",
|
||||
# "weight": 34.62,
|
||||
# "length": 1161,
|
||||
# "costTime": 10346,
|
||||
# "profileId": 35276233,
|
||||
# "plateIndex": 1,
|
||||
# "plateName": "",
|
||||
# "deviceId": "REDACTED",
|
||||
# "amsDetailMapping": [
|
||||
# {
|
||||
# "ams": 4,
|
||||
# "sourceColor": "F4D976FF",
|
||||
# "targetColor": "F4D976FF",
|
||||
# "filamentId": "GFL99",
|
||||
# "filamentType": "PLA",
|
||||
# "targetFilamentType": "",
|
||||
# "weight": 34.62
|
||||
# }
|
||||
# ],
|
||||
# "mode": "cloud_file",
|
||||
# "isPublicProfile": false,
|
||||
# "isPrintable": true,
|
||||
# "deviceModel": "P1P",
|
||||
# "deviceName": "Bambu P1P",
|
||||
# "bedType": "textured_plate"
|
||||
# },
|
||||
|
||||
def get_tasklist(self) -> dict:
|
||||
LOGGER.debug("Getting full task list from Bambu Cloud")
|
||||
try:
|
||||
response = self._get(BambuUrl.TASKS)
|
||||
except:
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
# Returns a list of projects for the account.
|
||||
#
|
||||
# {
|
||||
# "message": "success",
|
||||
# "code": null,
|
||||
# "error": null,
|
||||
# "projects": [
|
||||
# {
|
||||
# "project_id": "164995388",
|
||||
# "user_id": "1688388450",
|
||||
# "model_id": "US48e2103d939bf8",
|
||||
# "status": "ACTIVE",
|
||||
# "name": "Alcohol_Marker_Storage_for_Copic,_Ohuhu_and_the_like",
|
||||
# "content": "{'printed_plates': [{'plate': 1}]}",
|
||||
# "create_time": "2024-11-17 06:12:33",
|
||||
# "update_time": "2024-11-17 06:12:40"
|
||||
# },
|
||||
# ...
|
||||
#
|
||||
def get_projects(self) -> dict:
|
||||
LOGGER.debug("Getting projects list from Bambu Cloud")
|
||||
try:
|
||||
response = self._get(BambuUrl.PROJECTS)
|
||||
except:
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
def get_latest_task_for_printer(self, deviceId: str) -> dict:
|
||||
LOGGER.debug(f"Getting latest task for printer from Bambu Cloud")
|
||||
try:
|
||||
data = self.get_tasklist_for_printer(deviceId)
|
||||
if len(data) != 0:
|
||||
return data[0]
|
||||
LOGGER.debug("No tasks found for printer")
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_tasklist_for_printer(self, deviceId: str) -> dict:
|
||||
LOGGER.debug(f"Getting full task list for printer from Bambu Cloud")
|
||||
tasks = []
|
||||
data = self.get_tasklist()
|
||||
for task in data['hits']:
|
||||
if task['deviceId'] == deviceId:
|
||||
tasks.append(task)
|
||||
return tasks
|
||||
|
||||
def get_device_type_from_device_product_name(self, device_product_name: str):
|
||||
if device_product_name == "X1 Carbon":
|
||||
return "X1C"
|
||||
return device_product_name.replace(" ", "")
|
||||
|
||||
def download(self, url: str) -> bytearray:
|
||||
LOGGER.debug(f"Downloading cover image: {url}")
|
||||
try:
|
||||
# This is just a standard download from an unauthenticated end point.
|
||||
response = requests.get(url)
|
||||
except:
|
||||
return None
|
||||
return response.content
|
||||
|
||||
@property
|
||||
def username(self):
|
||||
return self._username
|
||||
|
||||
@property
|
||||
def auth_token(self):
|
||||
return self._auth_token
|
||||
|
||||
@property
|
||||
def bambu_connected(self) -> bool:
|
||||
return self._auth_token != "" and self._auth_token != None
|
||||
|
||||
@property
|
||||
def cloud_mqtt_host(self):
|
||||
return "cn.mqtt.bambulab.com" if self._region == "China" else "us.mqtt.bambulab.com"
|
24
octoprint_bambu_printer/printer/pybambu/commands.py
Normal file
24
octoprint_bambu_printer/printer/pybambu/commands.py
Normal file
@ -0,0 +1,24 @@
|
||||
"""MQTT Commands"""
|
||||
CHAMBER_LIGHT_ON = {
|
||||
"system": {"sequence_id": "0", "command": "ledctrl", "led_node": "chamber_light", "led_mode": "on",
|
||||
"led_on_time": 500, "led_off_time": 500, "loop_times": 0, "interval_time": 0}}
|
||||
CHAMBER_LIGHT_OFF = {
|
||||
"system": {"sequence_id": "0", "command": "ledctrl", "led_node": "chamber_light", "led_mode": "off",
|
||||
"led_on_time": 500, "led_off_time": 500, "loop_times": 0, "interval_time": 0}}
|
||||
|
||||
SPEED_PROFILE_TEMPLATE = {"print": {"sequence_id": "0", "command": "print_speed", "param": ""}}
|
||||
|
||||
GET_VERSION = {"info": {"sequence_id": "0", "command": "get_version"}}
|
||||
|
||||
PAUSE = {"print": {"sequence_id": "0", "command": "pause"}}
|
||||
RESUME = {"print": {"sequence_id": "0", "command": "resume"}}
|
||||
STOP = {"print": {"sequence_id": "0", "command": "stop"}}
|
||||
|
||||
PUSH_ALL = {"pushing": {"sequence_id": "0", "command": "pushall"}}
|
||||
|
||||
START_PUSH = { "pushing": {"sequence_id": "0", "command": "start"}}
|
||||
|
||||
SEND_GCODE_TEMPLATE = {"print": {"sequence_id": "0", "command": "gcode_line", "param": ""}} # param = GCODE_EACH_LINE_SEPARATED_BY_\n
|
||||
|
||||
# X1 only currently
|
||||
GET_ACCESSORIES = {"system": {"sequence_id": "0", "command": "get_accessories", "accessory_type": "none"}}
|
199
octoprint_bambu_printer/printer/pybambu/const.py
Normal file
199
octoprint_bambu_printer/printer/pybambu/const.py
Normal file
@ -0,0 +1,199 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
from enum import (
|
||||
Enum,
|
||||
IntEnum,
|
||||
)
|
||||
|
||||
from .const_hms_errors import HMS_ERRORS
|
||||
# These errors cover those that are AMS and/or slot specific.
|
||||
# 070X_xYxx_xxxx_xxxx = AMS X (0 based index) Slot Y (0 based index) has the error.
|
||||
from .const_ams_errors import HMS_AMS_ERRORS
|
||||
from .const_print_errors import PRINT_ERROR_ERRORS
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
|
||||
class Features(Enum):
|
||||
AUX_FAN = 1,
|
||||
CHAMBER_LIGHT = 2,
|
||||
CHAMBER_FAN = 3,
|
||||
CHAMBER_TEMPERATURE = 4,
|
||||
CURRENT_STAGE = 5,
|
||||
PRINT_LAYERS = 6,
|
||||
AMS = 7,
|
||||
EXTERNAL_SPOOL = 8,
|
||||
K_VALUE = 9,
|
||||
START_TIME = 10,
|
||||
AMS_TEMPERATURE = 11,
|
||||
CAMERA_RTSP = 13,
|
||||
START_TIME_GENERATED = 14,
|
||||
CAMERA_IMAGE = 15,
|
||||
DOOR_SENSOR = 16,
|
||||
MANUAL_MODE = 17,
|
||||
AMS_FILAMENT_REMAINING = 18,
|
||||
SET_TEMPERATURE = 19,
|
||||
|
||||
|
||||
class FansEnum(Enum):
|
||||
PART_COOLING = 1,
|
||||
AUXILIARY = 2,
|
||||
CHAMBER = 3,
|
||||
HEATBREAK = 4,
|
||||
|
||||
|
||||
class TempEnum(Enum):
|
||||
HEATBED = 1,
|
||||
NOZZLE = 2
|
||||
|
||||
|
||||
CURRENT_STAGE_IDS = {
|
||||
"default": "unknown",
|
||||
0: "printing",
|
||||
1: "auto_bed_leveling",
|
||||
2: "heatbed_preheating",
|
||||
3: "sweeping_xy_mech_mode",
|
||||
4: "changing_filament",
|
||||
5: "m400_pause",
|
||||
6: "paused_filament_runout",
|
||||
7: "heating_hotend",
|
||||
8: "calibrating_extrusion",
|
||||
9: "scanning_bed_surface",
|
||||
10: "inspecting_first_layer",
|
||||
11: "identifying_build_plate_type",
|
||||
12: "calibrating_micro_lidar", # DUPLICATED?
|
||||
13: "homing_toolhead",
|
||||
14: "cleaning_nozzle_tip",
|
||||
15: "checking_extruder_temperature",
|
||||
16: "paused_user",
|
||||
17: "paused_front_cover_falling",
|
||||
18: "calibrating_micro_lidar", # DUPLICATED?
|
||||
19: "calibrating_extrusion_flow",
|
||||
20: "paused_nozzle_temperature_malfunction",
|
||||
21: "paused_heat_bed_temperature_malfunction",
|
||||
22: "filament_unloading",
|
||||
23: "paused_skipped_step",
|
||||
24: "filament_loading",
|
||||
25: "calibrating_motor_noise",
|
||||
26: "paused_ams_lost",
|
||||
27: "paused_low_fan_speed_heat_break",
|
||||
28: "paused_chamber_temperature_control_error",
|
||||
29: "cooling_chamber",
|
||||
30: "paused_user_gcode",
|
||||
31: "motor_noise_showoff",
|
||||
32: "paused_nozzle_filament_covered_detected",
|
||||
33: "paused_cutter_error",
|
||||
34: "paused_first_layer_error",
|
||||
35: "paused_nozzle_clog",
|
||||
# X1 returns -1 for idle
|
||||
-1: "idle", # DUPLICATED
|
||||
# P1 returns 255 for idle
|
||||
255: "idle", # DUPLICATED
|
||||
}
|
||||
|
||||
CURRENT_STAGE_OPTIONS = list(set(CURRENT_STAGE_IDS.values())) # Conversion to set first removes the duplicates
|
||||
|
||||
GCODE_STATE_OPTIONS = [
|
||||
"failed",
|
||||
"finish",
|
||||
"idle",
|
||||
"init",
|
||||
"offline",
|
||||
"pause",
|
||||
"prepare",
|
||||
"running",
|
||||
"slicing",
|
||||
"unknown"
|
||||
]
|
||||
|
||||
SPEED_PROFILE = {
|
||||
1: "silent",
|
||||
2: "standard",
|
||||
3: "sport",
|
||||
4: "ludicrous"
|
||||
}
|
||||
|
||||
PRINT_TYPE_OPTIONS = {
|
||||
"cloud",
|
||||
"local",
|
||||
"idle",
|
||||
"system",
|
||||
"unknown"
|
||||
}
|
||||
|
||||
|
||||
def load_dict(filename: str) -> dict:
|
||||
with open(filename) as f:
|
||||
return json.load(f);
|
||||
|
||||
|
||||
FILAMENT_NAMES = load_dict(Path(__file__).with_name('filaments.json'))
|
||||
|
||||
HMS_SEVERITY_LEVELS = {
|
||||
"default": "unknown",
|
||||
1: "fatal",
|
||||
2: "serious",
|
||||
3: "common",
|
||||
4: "info"
|
||||
}
|
||||
|
||||
HMS_MODULES = {
|
||||
"default": "unknown",
|
||||
0x05: "mainboard",
|
||||
0x0C: "xcam",
|
||||
0x07: "ams",
|
||||
0x08: "toolhead",
|
||||
0x03: "mc"
|
||||
}
|
||||
|
||||
class SdcardState(Enum):
|
||||
NO_SDCARD = 0x00000000,
|
||||
HAS_SDCARD_NORMAL = 0x00000100,
|
||||
HAS_SDCARD_ABNORMAL = 0x00000200,
|
||||
SDCARD_STATE_NUM = 0x00000300,
|
||||
|
||||
class Home_Flag_Values(IntEnum):
|
||||
X_AXIS = 0x00000001,
|
||||
Y_AXIS = 0x00000002,
|
||||
Z_AXIS = 0x00000004,
|
||||
VOLTAGE220 = 0x00000008,
|
||||
XCAM_AUTO_RECOVERY_STEP_LOSS = 0x00000010,
|
||||
CAMERA_RECORDING = 0x00000020,
|
||||
# Gap
|
||||
AMS_CALIBRATE_REMAINING = 0x00000080,
|
||||
SD_CARD_PRESENT = 0x00000100,
|
||||
SD_CARD_ABNORMAL = 0x00000200,
|
||||
AMS_AUTO_SWITCH = 0x00000400,
|
||||
# Gap
|
||||
XCAM_ALLOW_PROMPT_SOUND = 0x00020000,
|
||||
WIRED_NETWORK = 0x00040000,
|
||||
FILAMENT_TANGLE_DETECT_SUPPORTED = 0x00080000,
|
||||
FILAMENT_TANGLE_DETECTED = 0x00100000,
|
||||
SUPPORTS_MOTOR_CALIBRATION = 0x00200000,
|
||||
# Gap
|
||||
DOOR_OPEN = 0x00800000,
|
||||
# Gap
|
||||
INSTALLED_PLUS = 0x04000000,
|
||||
SUPPORTED_PLUS = 0x08000000,
|
||||
# Gap
|
||||
|
||||
class BambuUrl(Enum):
|
||||
LOGIN = 1,
|
||||
TFA_LOGIN = 2,
|
||||
EMAIL_CODE = 3,
|
||||
BIND = 4,
|
||||
SLICER_SETTINGS = 5,
|
||||
TASKS = 6,
|
||||
PROJECTS = 7,
|
||||
|
||||
BAMBU_URL = {
|
||||
BambuUrl.LOGIN: 'https://api.bambulab.com/v1/user-service/user/login',
|
||||
BambuUrl.TFA_LOGIN: 'https://bambulab.com/api/sign-in/tfa',
|
||||
BambuUrl.EMAIL_CODE: 'https://api.bambulab.com/v1/user-service/user/sendemail/code',
|
||||
BambuUrl.BIND: 'https://api.bambulab.com/v1/iot-service/api/user/bind',
|
||||
BambuUrl.SLICER_SETTINGS: 'https://api.bambulab.com/v1/iot-service/api/slicer/setting?version=1.10.0.89',
|
||||
BambuUrl.TASKS: 'https://api.bambulab.com/v1/user-service/my/tasks',
|
||||
BambuUrl.PROJECTS: 'https://api.bambulab.com/v1/iot-service/api/user/project',
|
||||
}
|
386
octoprint_bambu_printer/printer/pybambu/const_ams_errors.py
Normal file
386
octoprint_bambu_printer/printer/pybambu/const_ams_errors.py
Normal file
@ -0,0 +1,386 @@
|
||||
HMS_AMS_ERRORS = {
|
||||
"0700_0100_0001_0001": "The AMS A assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
|
||||
"0700_0100_0001_0003": "The AMS A assist motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0700_0100_0001_0004": "The AMS A assist motor speed control is malfunctioning. The speed sensor may be faulty.",
|
||||
"0700_0100_0001_0011": "AMS A The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
|
||||
"0700_0100_0002_0002": "The AMS A assist motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0700_0200_0001_0001": "AMS A Filament speed and length error: The filament odometry may be faulty.",
|
||||
"0700_1000_0001_0001": "The AMS A slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"0700_1000_0001_0003": "The AMS A slot1 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0700_1000_0002_0002": "The AMS A slot1 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0700_1100_0001_0001": "The AMS A slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"0700_1100_0001_0003": "The AMS A slot2 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0700_1100_0002_0002": "The AMS A slot2 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0700_1200_0001_0001": "The AMS A slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"0700_1200_0001_0003": "The AMS A slot3 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0700_1200_0002_0002": "The AMS A slot3 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0700_1300_0001_0001": "The AMS A slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"0700_1300_0001_0003": "The AMS A slot4 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0700_1300_0002_0002": "The AMS A slot4 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0700_2000_0002_0001": "AMS A Slot1 filament has run out. Please insert a new filament.",
|
||||
"0700_2000_0002_0002": "AMS A Slot1 is empty; please insert a new filament.",
|
||||
"0700_2000_0002_0003": "AMS A Slot1's filament may be broken in AMS.",
|
||||
"0700_2000_0002_0004": "AMS A Slot1 filament may be broken in the tool head.",
|
||||
"0700_2000_0002_0005": "AMS A Slot1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
|
||||
"0700_2000_0002_0006": "PTFE tube detached during the feeding process. Please check if the PTFE tube is connected to the AMS and extruder.",
|
||||
"0700_2000_0003_0001": "AMS A Slot1 filament has run out. Please wait while old filament is purged.",
|
||||
"0700_2000_0003_0002": "AMS A Slot1 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"0700_2100_0002_0001": "AMS A Slot2 filament has run out. Please insert a new filament.",
|
||||
"0700_2100_0002_0002": "AMS A Slot2 is empty; please insert a new filament.",
|
||||
"0700_2100_0002_0003": "AMS A Slot2's filament may be broken in AMS.",
|
||||
"0700_2100_0002_0004": "AMS A Slot2 filament may be broken in the tool head.",
|
||||
"0700_2100_0002_0005": "AMS A Slot2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
|
||||
"0700_2100_0003_0001": "AMS A Slot2 filament has run out. Please wait while old filament is purged.",
|
||||
"0700_2100_0003_0002": "AMS A Slot2 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"0700_2200_0002_0001": "AMS A Slot3 filament has run out. Please insert a new filament.",
|
||||
"0700_2200_0002_0002": "AMS A Slot3 is empty; please insert a new filament.",
|
||||
"0700_2200_0002_0003": "AMS A Slot3's filament may be broken in AMS.",
|
||||
"0700_2200_0002_0004": "AMS A Slot3 filament may be broken in the tool head.",
|
||||
"0700_2200_0002_0005": "AMS A Slot3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
|
||||
"0700_2200_0003_0001": "AMS A Slot3 filament has run out. Please wait while old filament is purged.",
|
||||
"0700_2200_0003_0002": "AMS A Slot3 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"0700_2300_0002_0001": "AMS A Slot4 filament has run out. Please insert a new filament.",
|
||||
"0700_2300_0002_0002": "AMS A Slot4 is empty; please insert a new filament.",
|
||||
"0700_2300_0002_0003": "AMS A Slot4's filament may be broken in AMS.",
|
||||
"0700_2300_0002_0004": "AMS A Slot4 filament may be broken in the tool head.",
|
||||
"0700_2300_0002_0005": "AMS A Slot4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
|
||||
"0700_2300_0003_0001": "AMS A Slot4 filament has run out. Please wait while old filament is purged.",
|
||||
"0700_2300_0003_0002": "AMS A Slot4 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"0700_3000_0001_0001": "The AMS A RFID 0 board has an error.",
|
||||
"0700_3000_0001_0004": "Encryption chip failure.",
|
||||
"0700_3000_0002_0002": "The RFID-tag on AMS A Slot1 is damaged or the it's content cannot be identified.",
|
||||
"0700_3000_0003_0003": "RFID cannot be read because of a hardware or structural error.",
|
||||
"0700_3100_0001_0001": "The AMS A RFID 1 board has an error.",
|
||||
"0700_3100_0001_0004": "Encryption chip failure.",
|
||||
"0700_3100_0002_0002": "The RFID-tag on AMS A Slot2 is damaged or the it's content cannot be identified.",
|
||||
"0700_3100_0003_0003": "RFID cannot be read because of a hardware or structural error.",
|
||||
"0700_3200_0002_0002": "The RFID-tag on AMS A Slot3 is damaged or the it's content cannot be identified.",
|
||||
"0700_3300_0002_0002": "The RFID-tag on AMS A Slot4 is damaged or the it's content cannot be identified.",
|
||||
"0700_3500_0001_0001": "The temperature and humidity sensor has an error. The chip may be faulty.",
|
||||
"0700_3500_0001_0002": "AMS A The humidity sensor is disconnected, which may due to poor connector contact.",
|
||||
"0700_4000_0002_0001": "The filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
|
||||
"0700_4000_0002_0002": "The filament buffer position signal error: the position sensor may be malfunctioning.",
|
||||
"0700_4000_0002_0003": "The AMS Hub communication is abnormal, the cable may be not well connected.",
|
||||
"0700_4000_0002_0004": "The filament buffer signal is abnormal; the spring may be stuck or the filament may be tangle.",
|
||||
"0700_4500_0002_0001": "The filament cutter sensor is malfunctioning; please check whether the connector is properly plugged in.",
|
||||
"0700_4500_0002_0002": "The filament cutter's cutting distance is too large. The XY motor may lose steps.",
|
||||
"0700_4500_0002_0003": "The filament cutter handle has not been released. The handle or blade may be jammed, or there could be an issue with the filament sensor connection.",
|
||||
"0700_5000_0002_0001": "AMS A communication is abnormal; please check the connection cable.",
|
||||
"0700_5100_0003_0001": "The AMS is disabled; please load filament from spool holder.",
|
||||
"0700_6000_0002_0001": "The AMS A slot1 is overloaded. The filament may be tangled or the spool may be stuck.",
|
||||
"0700_6100_0002_0001": "The AMS A slot2 is overloaded. The filament may be tangled or the spool may be stuck.",
|
||||
"0700_6200_0002_0001": "The AMS A slot3 is overloaded. The filament may be tangled or the spool may be stuck.",
|
||||
"0700_6300_0002_0001": "The AMS A slot4 is overloaded. The filament may be tangled or the spool may be stuck.",
|
||||
"0700_7000_0002_0001": "Failed to pull out the filament from the extruder. Possible causes: clogged extruder or broken filament. Refer to the Assistant for details.",
|
||||
"0700_7000_0002_0002": "Failed to feed filament into the toolhead. Possible cause: filament or spool stuck. Please refer to the Assistant for details.",
|
||||
"0700_7000_0002_0003": "Failed to extrude the filament. Possible cause: extruder or nozzle clogged. Please refer to the Assistant for details.",
|
||||
"0700_7000_0002_0004": "Failed to pull back the filament from the toolhead to AMS. Possible cause: filament or spool stuck. Please refer to the Assistant for details.",
|
||||
"0700_7000_0002_0005": "Failed to feed the filament outside the AMS. Please clip the end of the filament flat and check to see if the spool is stuck. Please refer to the Assistant for details.",
|
||||
"0700_7000_0002_0006": "Timeout purging old filament. Possible cause: filament stuck or extruder/nozzle clogged. Please refer to the Assistant for details.",
|
||||
"0700_7000_0002_0007": "AMS filament ran out. Please put a new filament into AMS and click the 'Retry' button.",
|
||||
"0700_7000_0002_0008": "Failed to get AMS mapping table; please click 'Resume' to retry",
|
||||
"0700_7000_0002_0030": "",
|
||||
"0700_7000_0002_0031": "",
|
||||
"0700_7000_0002_0032": "",
|
||||
"0700_7000_0002_0033": "",
|
||||
"0700_7000_0002_0040": "",
|
||||
"0700_7000_0002_0041": "",
|
||||
"0700_7000_0002_0042": "",
|
||||
"0700_7000_0002_0043": "",
|
||||
"0700_7000_0002_0044": "",
|
||||
"0700_7000_0002_0045": "",
|
||||
"0700_7000_0002_0046": "",
|
||||
"0700_7000_0002_0050": "",
|
||||
"0700_7000_0002_0060": "",
|
||||
"0700_7000_0002_0061": "",
|
||||
"0700_8000_0001_0002": "AMS A The heater 1 is disconnected, which may due to poor connector contact.",
|
||||
"0700_8100_0001_0002": "AMS A The heater 2 is disconnected, which may due to poor connector contact.",
|
||||
"0700_9200_0001_0001": "AMS A The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
|
||||
"0700_9200_0002_0002": "AMS A The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
|
||||
"0700_9300_0001_0001": "AMS A The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
|
||||
"0700_9300_0002_0002": "AMS A The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
|
||||
"0700_9400_0001_0001": "AMS A The temperature sensor of heater 1 is malfunctioning, , which may due to poor connector contact.",
|
||||
"0700_9500_0001_0001": "AMS A The temperature sensor of heater 2 is malfunctioning, , which may due to poor connector contact.",
|
||||
"0701_0100_0001_0001": "The AMS B assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
|
||||
"0701_0100_0001_0003": "The AMS B assist motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0701_0100_0001_0004": "The AMS B assist motor speed control is malfunctioning. The speed sensor may be faulty.",
|
||||
"0701_0100_0001_0011": "AMS B The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
|
||||
"0701_0100_0002_0002": "The AMS B assist motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0701_0200_0001_0001": "AMS B Filament speed and length error: The filament odometry may be faulty.",
|
||||
"0701_1000_0001_0001": "The AMS B slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"0701_1000_0001_0003": "The AMS B slot1 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0701_1000_0002_0002": "The AMS B slot1 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0701_1100_0001_0001": "The AMS B slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"0701_1100_0001_0003": "The AMS B slot2 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0701_1100_0002_0002": "The AMS B slot2 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0701_1200_0001_0001": "The AMS B slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"0701_1200_0001_0003": "The AMS B slot3 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0701_1200_0002_0002": "The AMS B slot3 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0701_1300_0001_0001": "The AMS B slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"0701_1300_0001_0003": "The AMS B slot4 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0701_1300_0002_0002": "The AMS B slot4 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0701_2000_0002_0001": "AMS B Slot1 filament has run out. Please insert a new filament.",
|
||||
"0701_2000_0002_0002": "AMS B Slot1 is empty; please insert a new filament.",
|
||||
"0701_2000_0002_0003": "AMS B Slot1's filament may be broken in AMS.",
|
||||
"0701_2000_0002_0004": "AMS B Slot1 filament may be broken in the tool head.",
|
||||
"0701_2000_0002_0005": "AMS B Slot1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
|
||||
"0701_2000_0002_0006": "PTFE tube detached during the feeding process. Please check if the PTFE tube is connected to the AMS and extruder.",
|
||||
"0701_2000_0003_0001": "AMS B Slot1 filament has run out. Please wait while old filament is purged.",
|
||||
"0701_2000_0003_0002": "AMS B Slot1 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"0701_2100_0002_0001": "AMS B Slot2 filament has run out. Please insert a new filament.",
|
||||
"0701_2100_0002_0002": "AMS B Slot2 is empty; please insert a new filament.",
|
||||
"0701_2100_0002_0003": "AMS B Slot2's filament may be broken in AMS.",
|
||||
"0701_2100_0002_0004": "AMS B Slot2 filament may be broken in the tool head.",
|
||||
"0701_2100_0002_0005": "AMS B Slot2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
|
||||
"0701_2100_0003_0001": "AMS B Slot2 filament has run out. Please wait while old filament is purged.",
|
||||
"0701_2100_0003_0002": "AMS B Slot2 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"0701_2200_0002_0001": "AMS B Slot3 filament has run out. Please insert a new filament.",
|
||||
"0701_2200_0002_0002": "AMS B Slot3 is empty; please insert a new filament.",
|
||||
"0701_2200_0002_0003": "AMS B Slot3's filament may be broken in AMS.",
|
||||
"0701_2200_0002_0004": "AMS B Slot3 filament may be broken in the tool head.",
|
||||
"0701_2200_0002_0005": "AMS B Slot3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
|
||||
"0701_2200_0003_0001": "AMS B Slot3 filament has run out. Please wait while old filament is purged.",
|
||||
"0701_2200_0003_0002": "AMS B Slot3 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"0701_2300_0002_0001": "AMS B Slot4 filament has run out. Please insert a new filament.",
|
||||
"0701_2300_0002_0002": "AMS B Slot4 is empty; please insert a new filament.",
|
||||
"0701_2300_0002_0003": "AMS B Slot4's filament may be broken in AMS.",
|
||||
"0701_2300_0002_0004": "AMS B Slot4 filament may be broken in the tool head.",
|
||||
"0701_2300_0002_0005": "AMS B Slot4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
|
||||
"0701_2300_0003_0001": "AMS B Slot4 filament has run out. Please wait while old filament is purged.",
|
||||
"0701_2300_0003_0002": "AMS B Slot4 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"0701_3000_0001_0001": "The AMS B RFID 0 board has an error.",
|
||||
"0701_3000_0001_0004": "Encryption chip failure.",
|
||||
"0701_3000_0002_0002": "The RFID-tag on AMS B Slot1 is damaged or the it's content cannot be identified.",
|
||||
"0701_3000_0003_0003": "RFID cannot be read because of a hardware or structural error.",
|
||||
"0701_3100_0001_0001": "The AMS B RFID 1 board has an error.",
|
||||
"0701_3100_0001_0004": "Encryption chip failure.",
|
||||
"0701_3100_0002_0002": "The RFID-tag on AMS B Slot2 is damaged or the it's content cannot be identified.",
|
||||
"0701_3100_0003_0003": "RFID cannot be read because of a hardware or structural error.",
|
||||
"0701_3200_0002_0002": "The RFID-tag on AMS B Slot3 is damaged or the it's content cannot be identified.",
|
||||
"0701_3300_0002_0002": "The RFID-tag on AMS B Slot4 is damaged or the it's content cannot be identified.",
|
||||
"0701_3500_0001_0001": "The temperature and humidity sensor has an error. The chip may be faulty.",
|
||||
"0701_3500_0001_0002": "AMS B The humidity sensor is disconnected, which may due to poor connector contact.",
|
||||
"0701_5000_0002_0001": "AMS B communication is abnormal; please check the connection cable.",
|
||||
"0701_6000_0002_0001": "The AMS B slot1 is overloaded. The filament may be tangled or the spool may be stuck.",
|
||||
"0701_6100_0002_0001": "The AMS B slot2 is overloaded. The filament may be tangled or the spool may be stuck.",
|
||||
"0701_6200_0002_0001": "The AMS B slot3 is overloaded. The filament may be tangled or the spool may be stuck.",
|
||||
"0701_6300_0002_0001": "The AMS B slot4 is overloaded. The filament may be tangled or the spool may be stuck.",
|
||||
"0701_7000_0002_0001": "Failed to pull out the filament from the extruder. Possible causes: clogged extruder or broken filament. Refer to the Assistant for details.",
|
||||
"0701_7000_0002_0002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck. Please refer to the Assistant for details.",
|
||||
"0701_7000_0002_0003": "Failed to extrude the filament. Possible cause: extruder or nozzle clogged. Please refer to the Assistant for details.",
|
||||
"0701_7000_0002_0004": "Failed to pull back the filament from the toolhead to AMS. Possible cause: filament or spool stuck. Please refer to the Assistant for details.",
|
||||
"0701_7000_0002_0005": "Failed to feed the filament outside the AMS. Please clip the end of the filament flat and check to see if the spool is stuck. Please refer to the Assistant for details.",
|
||||
"0701_7000_0002_0006": "Timeout purging old filament. Possible cause: filament stuck or extruder/nozzle clogged. Please refer to the Assistant for details.",
|
||||
"0701_7000_0002_0007": "AMS filament ran out. Please put a new filament into AMS and click the 'Retry' button.",
|
||||
"0701_7000_0002_0008": "Failed to get AMS mapping table; please click 'Resume' to retry",
|
||||
"0701_7000_0002_0030": "",
|
||||
"0701_7000_0002_0031": "",
|
||||
"0701_7000_0002_0032": "",
|
||||
"0701_7000_0002_0033": "",
|
||||
"0701_7000_0002_0040": "",
|
||||
"0701_7000_0002_0041": "",
|
||||
"0701_7000_0002_0042": "",
|
||||
"0701_7000_0002_0043": "",
|
||||
"0701_7000_0002_0044": "",
|
||||
"0701_7000_0002_0045": "",
|
||||
"0701_7000_0002_0046": "",
|
||||
"0701_7000_0002_0050": "",
|
||||
"0701_7000_0002_0060": "",
|
||||
"0701_7000_0002_0061": "",
|
||||
"0701_8000_0001_0002": "AMS B The heater 1 is disconnected, which may due to poor connector contact.",
|
||||
"0701_8100_0001_0002": "AMS B The heater 2 is disconnected, which may due to poor connector contact.",
|
||||
"0701_9200_0001_0001": "AMS B The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
|
||||
"0701_9200_0002_0002": "AMS B The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
|
||||
"0701_9300_0001_0001": "AMS B The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
|
||||
"0701_9300_0002_0002": "AMS B The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
|
||||
"0701_9400_0001_0001": "AMS B The temperature sensor of heater 1 is malfunctioning, , which may due to poor connector contact.",
|
||||
"0701_9500_0001_0001": "AMS B The temperature sensor of heater 2 is malfunctioning, , which may due to poor connector contact.",
|
||||
"0702_0100_0001_0001": "The AMS C assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
|
||||
"0702_0100_0001_0003": "The AMS C assist motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0702_0100_0001_0004": "The AMS C assist motor speed control is malfunctioning. The speed sensor may be faulty.",
|
||||
"0702_0100_0001_0011": "AMS C The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
|
||||
"0702_0100_0002_0002": "The AMS C assist motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0702_0200_0001_0001": "AMS C Filament speed and length error: The filament odometry may be faulty.",
|
||||
"0702_1000_0001_0001": "The AMS C slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"0702_1000_0001_0003": "The AMS C slot1 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0702_1000_0002_0002": "The AMS C slot1 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0702_1100_0001_0001": "The AMS C slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"0702_1100_0001_0003": "The AMS C slot2 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0702_1100_0002_0002": "The AMS C slot2 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0702_1200_0001_0001": "The AMS C slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"0702_1200_0001_0003": "The AMS C slot3 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0702_1200_0002_0002": "The AMS C slot3 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0702_1300_0001_0001": "The AMS C slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"0702_1300_0001_0003": "The AMS C slot4 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0702_1300_0002_0002": "The AMS C slot4 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0702_2000_0002_0001": "AMS C Slot1 filament has run out. Please insert a new filament.",
|
||||
"0702_2000_0002_0002": "AMS C Slot1 is empty; please insert a new filament.",
|
||||
"0702_2000_0002_0003": "AMS C Slot1's filament may be broken in AMS.",
|
||||
"0702_2000_0002_0004": "AMS C Slot1 filament may be broken in the tool head.",
|
||||
"0702_2000_0002_0005": "AMS C Slot1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
|
||||
"0702_2000_0002_0006": "PTFE tube detached during the feeding process. Please check if the PTFE tube is connected to the AMS and extruder.",
|
||||
"0702_2000_0003_0001": "AMS C Slot1 filament has run out. Please wait while old filament is purged.",
|
||||
"0702_2000_0003_0002": "AMS C Slot1 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"0702_2100_0002_0001": "AMS C Slot2 filament has run out. Please insert a new filament.",
|
||||
"0702_2100_0002_0002": "AMS C Slot2 is empty; please insert a new filament.",
|
||||
"0702_2100_0002_0003": "AMS C Slot2's filament may be broken in AMS.",
|
||||
"0702_2100_0002_0004": "AMS C Slot2 filament may be broken in the tool head.",
|
||||
"0702_2100_0002_0005": "AMS C Slot2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
|
||||
"0702_2100_0003_0001": "AMS C Slot2 filament has run out. Please wait while old filament is purged.",
|
||||
"0702_2100_0003_0002": "AMS C Slot2 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"0702_2200_0002_0001": "AMS C Slot3 filament has run out. Please insert a new filament.",
|
||||
"0702_2200_0002_0002": "AMS C Slot3 is empty; please insert a new filament.",
|
||||
"0702_2200_0002_0003": "AMS C Slot3's filament may be broken in AMS.",
|
||||
"0702_2200_0002_0004": "AMS C Slot3 filament may be broken in the tool head.",
|
||||
"0702_2200_0002_0005": "AMS C Slot3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
|
||||
"0702_2200_0003_0001": "AMS C Slot3 filament has run out. Please wait while old filament is purged.",
|
||||
"0702_2200_0003_0002": "AMS C Slot3 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"0702_2300_0002_0001": "AMS C Slot4 filament has run out. Please insert a new filament.",
|
||||
"0702_2300_0002_0002": "AMS C Slot4 is empty; please insert a new filament.",
|
||||
"0702_2300_0002_0003": "AMS C Slot4's filament may be broken in AMS.",
|
||||
"0702_2300_0002_0004": "AMS C Slot4 filament may be broken in the tool head.",
|
||||
"0702_2300_0002_0005": "AMS C Slot4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
|
||||
"0702_2300_0003_0001": "AMS C Slot4 filament has run out. Please wait while old filament is purged.",
|
||||
"0702_2300_0003_0002": "AMS C Slot4 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"0702_3000_0001_0001": "The AMS C RFID 0 board has an error.",
|
||||
"0702_3000_0001_0004": "Encryption chip failure.",
|
||||
"0702_3000_0002_0002": "The RFID-tag on AMS C Slot1 is damaged or the it's content cannot be identified.",
|
||||
"0702_3000_0003_0003": "RFID cannot be read because of a hardware or structural error.",
|
||||
"0702_3100_0001_0001": "The AMS C RFID 1 board has an error.",
|
||||
"0702_3100_0001_0004": "Encryption chip failure.",
|
||||
"0702_3100_0002_0002": "The RFID-tag on AMS C Slot2 is damaged or the it's content cannot be identified.",
|
||||
"0702_3100_0003_0003": "RFID cannot be read because of a hardware or structural error.",
|
||||
"0702_3200_0002_0002": "The RFID-tag on AMS C Slot3 is damaged or the it's content cannot be identified.",
|
||||
"0702_3300_0002_0002": "The RFID-tag on AMS C Slot4 is damaged or the it's content cannot be identified.",
|
||||
"0702_3500_0001_0001": "The temperature and humidity sensor has an error. The chip may be faulty.",
|
||||
"0702_3500_0001_0002": "AMS C The humidity sensor is disconnected, which may due to poor connector contact.",
|
||||
"0702_5000_0002_0001": "AMS C communication is abnormal; please check the connection cable.",
|
||||
"0702_6000_0002_0001": "The AMS C slot1 is overloaded. The filament may be tangled or the spool may be stuck.",
|
||||
"0702_6100_0002_0001": "The AMS C slot2 is overloaded. The filament may be tangled or the spool may be stuck.",
|
||||
"0702_6200_0002_0001": "The AMS C slot3 is overloaded. The filament may be tangled or the spool may be stuck.",
|
||||
"0702_6300_0002_0001": "The AMS C slot4 is overloaded. The filament may be tangled or the spool may be stuck.",
|
||||
"0702_7000_0002_0001": "Failed to pull out the filament from the extruder. Possible causes: clogged extruder or broken filament. Refer to the Assistant for details.",
|
||||
"0702_7000_0002_0002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck. Please refer to the Assistant for details.",
|
||||
"0702_7000_0002_0003": "Failed to extrude the filament. Possible cause: extruder or nozzle clogged. Please refer to the Assistant for details.",
|
||||
"0702_7000_0002_0004": "Failed to pull back the filament from the toolhead to AMS. Possible cause: filament or spool stuck. Please refer to the Assistant for details.",
|
||||
"0702_7000_0002_0005": "Failed to feed the filament outside the AMS. Please clip the end of the filament flat and check to see if the spool is stuck. Please refer to the Assistant for details.",
|
||||
"0702_7000_0002_0006": "Timeout purging old filament. Possible cause: filament stuck or extruder/nozzle clogged. Please refer to the Assistant for details.",
|
||||
"0702_7000_0002_0007": "AMS filament ran out. Please put a new filament into AMS and click the 'Retry' button.",
|
||||
"0702_7000_0002_0008": "Failed to get AMS mapping table; please click 'Resume' to retry",
|
||||
"0702_7000_0002_0030": "",
|
||||
"0702_7000_0002_0031": "",
|
||||
"0702_7000_0002_0032": "",
|
||||
"0702_7000_0002_0033": "",
|
||||
"0702_7000_0002_0040": "",
|
||||
"0702_7000_0002_0041": "",
|
||||
"0702_7000_0002_0042": "",
|
||||
"0702_7000_0002_0043": "",
|
||||
"0702_7000_0002_0044": "",
|
||||
"0702_7000_0002_0045": "",
|
||||
"0702_7000_0002_0046": "",
|
||||
"0702_7000_0002_0050": "",
|
||||
"0702_7000_0002_0060": "",
|
||||
"0702_7000_0002_0061": "",
|
||||
"0702_8000_0001_0002": "AMS C The heater 1 is disconnected, which may due to poor connector contact.",
|
||||
"0702_8100_0001_0002": "AMS C The heater 2 is disconnected, which may due to poor connector contact.",
|
||||
"0702_9200_0001_0001": "AMS C The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
|
||||
"0702_9200_0002_0002": "AMS C The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
|
||||
"0702_9300_0001_0001": "AMS C The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
|
||||
"0702_9300_0002_0002": "AMS C The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
|
||||
"0702_9400_0001_0001": "AMS C The temperature sensor of heater 1 is malfunctioning, , which may due to poor connector contact.",
|
||||
"0702_9500_0001_0001": "AMS C The temperature sensor of heater 2 is malfunctioning, , which may due to poor connector contact.",
|
||||
"0703_0100_0001_0001": "The AMS D assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
|
||||
"0703_0100_0001_0003": "The AMS D assist motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0703_0100_0001_0004": "The AMS D assist motor speed control is malfunctioning. The speed sensor may be faulty.",
|
||||
"0703_0100_0001_0011": "AMS D The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
|
||||
"0703_0100_0002_0002": "The AMS D assist motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0703_0200_0001_0001": "AMS D Filament speed and length error: The filament odometry may be faulty.",
|
||||
"0703_1000_0001_0001": "The AMS D slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"0703_1000_0001_0003": "The AMS D slot1 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0703_1000_0002_0002": "The AMS D slot1 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0703_1100_0001_0001": "The AMS D slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"0703_1100_0001_0003": "The AMS D slot2 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0703_1100_0002_0002": "The AMS D slot2 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0703_1200_0001_0001": "The AMS D slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"0703_1200_0001_0003": "The AMS D slot3 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0703_1200_0002_0002": "The AMS D slot3 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0703_1300_0001_0001": "The AMS D slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"0703_1300_0001_0003": "The AMS D slot4 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"0703_1300_0002_0002": "The AMS D slot4 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"0703_2000_0002_0001": "AMS D Slot1 filament has run out. Please insert a new filament.",
|
||||
"0703_2000_0002_0002": "AMS D Slot1 is empty; please insert a new filament.",
|
||||
"0703_2000_0002_0003": "AMS D Slot1's filament may be broken in AMS.",
|
||||
"0703_2000_0002_0004": "AMS D Slot1 filament may be broken in the tool head.",
|
||||
"0703_2000_0002_0005": "AMS D Slot1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
|
||||
"0703_2000_0002_0006": "PTFE tube detached during the feeding process. Please check if the PTFE tube is connected to the AMS and extruder.",
|
||||
"0703_2000_0003_0001": "AMS D Slot1 filament has run out. Please wait while old filament is purged.",
|
||||
"0703_2000_0003_0002": "AMS D Slot1 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"0703_2100_0002_0001": "AMS D Slot2 filament has run out. Please insert a new filament.",
|
||||
"0703_2100_0002_0002": "AMS D Slot2 is empty; please insert a new filament.",
|
||||
"0703_2100_0002_0003": "AMS D Slot2's filament may be broken in AMS.",
|
||||
"0703_2100_0002_0004": "AMS D Slot2 filament may be broken in the tool head.",
|
||||
"0703_2100_0002_0005": "AMS D Slot2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
|
||||
"0703_2100_0003_0001": "AMS D Slot2 filament has run out. Please wait while old filament is purged.",
|
||||
"0703_2100_0003_0002": "AMS D Slot2 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"0703_2200_0002_0001": "AMS D Slot3 filament has run out. Please insert a new filament.",
|
||||
"0703_2200_0002_0002": "AMS D Slot3 is empty; please insert a new filament.",
|
||||
"0703_2200_0002_0003": "AMS D Slot3's filament may be broken in AMS.",
|
||||
"0703_2200_0002_0004": "AMS D Slot3 filament may be broken in the tool head.",
|
||||
"0703_2200_0002_0005": "AMS D Slot3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
|
||||
"0703_2200_0003_0001": "AMS D Slot3 filament has run out. Please wait while old filament is purged.",
|
||||
"0703_2200_0003_0002": "AMS D Slot3 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"0703_2300_0002_0001": "AMS D Slot4 filament has run out. Please insert a new filament.",
|
||||
"0703_2300_0002_0002": "AMS D Slot4 is empty; please insert a new filament.",
|
||||
"0703_2300_0002_0003": "AMS D Slot4's filament may be broken in AMS.",
|
||||
"0703_2300_0002_0004": "AMS D Slot4 filament may be broken in the tool head.",
|
||||
"0703_2300_0002_0005": "AMS D Slot4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
|
||||
"0703_2300_0003_0001": "AMS D Slot4 filament has run out. Please wait while old filament is purged.",
|
||||
"0703_2300_0003_0002": "AMS D Slot4 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"0703_3000_0001_0001": "The AMS D RFID 0 board has an error.",
|
||||
"0703_3000_0001_0004": "Encryption chip failure.",
|
||||
"0703_3000_0002_0002": "The RFID-tag on AMS D Slot1 is damaged or the it's content cannot be identified.",
|
||||
"0703_3000_0003_0003": "RFID cannot be read because of a hardware or structural error.",
|
||||
"0703_3100_0001_0001": "The AMS D RFID 1 board has an error.",
|
||||
"0703_3100_0001_0004": "Encryption chip failure.",
|
||||
"0703_3100_0002_0002": "The RFID-tag on AMS D Slot2 is damaged or the it's content cannot be identified.",
|
||||
"0703_3100_0003_0003": "RFID cannot be read because of a hardware or structural error.",
|
||||
"0703_3200_0002_0002": "The RFID-tag on AMS D Slot3 is damaged or the it's content cannot be identified.",
|
||||
"0703_3300_0002_0002": "The RFID-tag on AMS D Slot4 is damaged or the it's content cannot be identified.",
|
||||
"0703_3500_0001_0001": "The temperature and humidity sensor has an error. The chip may be faulty.",
|
||||
"0703_3500_0001_0002": "AMS D The humidity sensor is disconnected, which may due to poor connector contact.",
|
||||
"0703_5000_0002_0001": "AMS D communication is abnormal; please check the connection cable.",
|
||||
"0703_6000_0002_0001": "The AMS D slot1 is overloaded. The filament may be tangled or the spool may be stuck.",
|
||||
"0703_6100_0002_0001": "The AMS D slot2 is overloaded. The filament may be tangled or the spool may be stuck.",
|
||||
"0703_6200_0002_0001": "The AMS D slot3 is overloaded. The filament may be tangled or the spool may be stuck.",
|
||||
"0703_6300_0002_0001": "The AMS D slot4 is overloaded. The filament may be tangled or the spool may be stuck.",
|
||||
"0703_7000_0002_0001": "Failed to pull out the filament from the extruder. Possible causes: clogged extruder or broken filament. Refer to the Assistant for details.",
|
||||
"0703_7000_0002_0002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck. Please refer to the Assistant for details.",
|
||||
"0703_7000_0002_0003": "Failed to extrude the filament. Possible cause: extruder or nozzle clogged. Please refer to the Assistant for details.",
|
||||
"0703_7000_0002_0004": "Failed to pull back the filament from the toolhead to AMS. Possible cause: filament or spool stuck. Please refer to the Assistant for details.",
|
||||
"0703_7000_0002_0005": "Failed to feed the filament outside the AMS. Please clip the end of the filament flat and check to see if the spool is stuck. Please refer to the Assistant for details.",
|
||||
"0703_7000_0002_0006": "Timeout purging old filament. Possible cause: filament stuck or extruder/nozzle clogged. Please refer to the Assistant for details.",
|
||||
"0703_7000_0002_0007": "AMS filament ran out. Please put a new filament into AMS and click the 'Retry' button.",
|
||||
"0703_7000_0002_0008": "Failed to get AMS mapping table; please click 'Resume' to retry",
|
||||
"0703_7000_0002_0030": "",
|
||||
"0703_7000_0002_0031": "",
|
||||
"0703_7000_0002_0032": "",
|
||||
"0703_7000_0002_0033": "",
|
||||
"0703_7000_0002_0040": "",
|
||||
"0703_7000_0002_0041": "",
|
||||
"0703_7000_0002_0042": "",
|
||||
"0703_7000_0002_0043": "",
|
||||
"0703_7000_0002_0044": "",
|
||||
"0703_7000_0002_0045": "",
|
||||
"0703_7000_0002_0046": "",
|
||||
"0703_7000_0002_0050": "",
|
||||
"0703_7000_0002_0060": "",
|
||||
"0703_7000_0002_0061": "",
|
||||
"0703_8000_0001_0002": "AMS D The heater 1 is disconnected, which may due to poor connector contact.",
|
||||
"0703_8100_0001_0002": "AMS D The heater 2 is disconnected, which may due to poor connector contact.",
|
||||
"0703_9200_0001_0001": "AMS D The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
|
||||
"0703_9200_0002_0002": "AMS D The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
|
||||
"0703_9300_0001_0001": "AMS D The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
|
||||
"0703_9300_0002_0002": "AMS D The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
|
||||
"0703_9400_0001_0001": "AMS D The temperature sensor of heater 1 is malfunctioning, , which may due to poor connector contact.",
|
||||
"0703_9500_0001_0001": "AMS D The temperature sensor of heater 2 is malfunctioning, , which may due to poor connector contact.",
|
||||
}
|
528
octoprint_bambu_printer/printer/pybambu/const_hms_errors.py
Normal file
528
octoprint_bambu_printer/printer/pybambu/const_hms_errors.py
Normal file
@ -0,0 +1,528 @@
|
||||
HMS_ERRORS = {
|
||||
"0300_0100_0001_0001": "The heatbed temperature is abnormal; the heater may have a short circuit.",
|
||||
"0300_0100_0001_0002": "The heatbed temperature is abnormal; the heater may have an open circuit, or the thermal switch may be open.",
|
||||
"0300_0100_0001_0003": "The heatbed temperature is abnormal; the heater is over temperature.",
|
||||
"0300_0100_0001_0006": "The heatbed temperature is abnormal; the sensor may have a short circuit.",
|
||||
"0300_0100_0001_0007": "The heatbed temperature is abnormal; the sensor may have an open circuit.",
|
||||
"0300_0100_0001_0008": "An abnormality occurs during the heating process of the heatbed, the heating modules may be broken.",
|
||||
"0300_0100_0001_000A": "The heatbed temperature control is abnormal, the AC board may be broken.",
|
||||
"0300_0100_0001_000C": "The heatbed has worked at full load for a long time. The temperature control system may be abnormal.",
|
||||
"0300_0100_0001_000D": "An issue occurred when heating the heatbed previously. To continue using your printer, please refer to the wiki to troubleshoot.",
|
||||
"0300_0100_0003_0008": "The temperature of the heated bed exceeds the limit and automatically adjusts to the limit temperature.",
|
||||
"0300_0200_0001_0001": "The nozzle temperature is abnormal; the heater may have a short circuit.",
|
||||
"0300_0200_0001_0002": "The nozzle temperature is abnormal; the heater may have an open circuit.",
|
||||
"0300_0200_0001_0003": "The nozzle temperature is abnormal; the heater is over temperature.",
|
||||
"0300_0200_0001_0006": "The nozzle temperature is abnormal; the sensor may have a short circuit. Please check whether the connector is properly plugged in.",
|
||||
"0300_0200_0001_0007": "The nozzle temperature is abnormal; the sensor may have an open circuit.",
|
||||
"0300_0200_0001_0009": "The nozzle temperature control is abnormal; the hot end may not be installed. If you want to heat the hot end without it being installed, please turn on maintenance mode on the settings page.",
|
||||
"0300_0200_0001_000B": "The nozzle temperature is abnormal. Temperature control system may have an issue.",
|
||||
"0300_0300_0001_0001": "The hotend cooling fan speed is too slow or stopped. It may be stuck or the connector is not plugged in properly.",
|
||||
"0300_0300_0002_0002": "",
|
||||
"0300_0400_0002_0001": "The speed of the part cooling fan is too slow or stopped. It may be stuck or the connector is not plugged in properly.",
|
||||
"0300_0500_0001_0001": "The motor driver is overheating. Its radiator may be loose, or its cooling fan may be damaged.",
|
||||
"0300_0600_0001_0001": "Motor-A has an open-circuit. There may be a loose connection, or the motor may have failed.",
|
||||
"0300_0600_0001_0002": "Motor-A has a short-circuit. It may have failed.",
|
||||
"0300_0600_0001_0003": "The resistance of Motor-A is abnormal, the motor may have failed.",
|
||||
"0300_0700_0001_0001": "Motor-B has an open-circuit. The connection may be loose, or the motor may have failed.",
|
||||
"0300_0700_0001_0002": "Motor-B has a short-circuit. It may have failed.",
|
||||
"0300_0700_0001_0003": "The resistance of Motor-B is abnormal, the motor may have failed.",
|
||||
"0300_0800_0001_0001": "Motor-Z has an open-circuit. The connection may be loose, or the motor may have failed.",
|
||||
"0300_0800_0001_0002": "Motor-Z has a short-circuit. It may have failed.",
|
||||
"0300_0800_0001_0003": "The resistance of Motor-Z is abnormal, the motor may have failed.",
|
||||
"0300_0900_0001_0001": "Motor-E has an open-circuit. The connection may be loose, or the motor may have failed.",
|
||||
"0300_0900_0001_0002": "Motor-E has a short-circuit. It may have failed.",
|
||||
"0300_0900_0001_0003": "The resistance of Motor-E is abnormal, the motor may have failed.",
|
||||
"0300_0A00_0001_0001": "Heatbed force sensor 1 is too sensitive. It may be stuck between the strain arm and heatbed support, or the adjusting screw may be too tight.",
|
||||
"0300_0A00_0001_0002": "The signal of heatbed force sensor 1 is weak. The force sensor may be broken or have poor electric connection.",
|
||||
"0300_0A00_0001_0003": "The signal of heatbed force sensor 1 is too weak. The electronic connection to the sensor may be broken.",
|
||||
"0300_0A00_0001_0004": "An external disturbance was detected on force sensor 1. The heatbed plate may have touched something outside the heatbed.",
|
||||
"0300_0A00_0001_0005": "Force sensor 1 detected unexpected continuous force. The heatbed may be stuck, or the analog front end may be broken.",
|
||||
"0300_0B00_0001_0001": "Heatbed force sensor 2 is too sensitive. It may be stuck between the strain arm and heatbed support, or the adjusting screw may be too tight.",
|
||||
"0300_0B00_0001_0002": "The signal of heatbed force sensor 2 is weak. The force sensor may be broken or have poor electric connection.",
|
||||
"0300_0B00_0001_0003": "The signal of heatbed force sensor 2 is too weak. The electronic connection to the sensor may be broken.",
|
||||
"0300_0B00_0001_0004": "An external disturbance was detected on force sensor 2. The heatbed plate may have touched something outside the heatbed.",
|
||||
"0300_0B00_0001_0005": "Force sensor 2 detected unexpected continuous force. The heatbed may be stuck, or the analog front end may be broken.",
|
||||
"0300_0C00_0001_0001": "Heatbed force sensor 3 is too sensitive. It may be stuck between the strain arm and heatbed support, or the adjusting screw may be too tight.",
|
||||
"0300_0C00_0001_0002": "The signal of heatbed force sensor 3 is weak. The force sensor may be broken or have poor electric connection.",
|
||||
"0300_0C00_0001_0003": "The signal of heatbed force sensor 3 is too weak. The electronic connection to the sensor may be broken.",
|
||||
"0300_0C00_0001_0004": "An external disturbance was detected on force sensor 3. The heatbed plate may have touched something outside the heatbed.",
|
||||
"0300_0C00_0001_0005": "Force sensor 3 detected unexpected continuous force. The heatbed may be stuck, or the analog front end may be broken.",
|
||||
"0300_0D00_0001_0002": "Heatbed homing failed. The environmental vibration is too great.",
|
||||
"0300_0D00_0001_0003": "The build plate is not placed properly. Please adjust it.",
|
||||
"0300_0D00_0001_0004": "The build plate is not placed properly. Please adjust it.",
|
||||
"0300_0D00_0001_0005": "The build plate is not placed properly. Please adjust it.",
|
||||
"0300_0D00_0001_0006": "The build plate is not placed properly. Please adjust it.",
|
||||
"0300_0D00_0001_0007": "The build plate is not placed properly. Please adjust it.",
|
||||
"0300_0D00_0001_0008": "The build plate is not placed properly. Please adjust it.",
|
||||
"0300_0D00_0001_0009": "The build plate is not placed properly. Please adjust it.",
|
||||
"0300_0D00_0001_000A": "The build plate is not placed properly. Please adjust it.",
|
||||
"0300_0D00_0001_000B": "The Z axis motor seems to be stuck when moving. Please check if there is any foreign matter on the Z sliders or Z timing belt wheels .",
|
||||
"0300_0D00_0002_0001": "Heatbed homing abnormal: there may be a bulge on the heatbed or the nozzle tip may not be clean.",
|
||||
"0300_0D00_0002_0003": "The build plate may not be properly placed. If this message appears repeatedly, please check the Wiki for more reasons.",
|
||||
"0300_0D00_0002_0004": "The build plate may not be properly placed. If this message appears repeatedly, please check the Wiki for more reasons.",
|
||||
"0300_0D00_0002_0005": "The build plate may not be properly placed. If this message appears repeatedly, please check the Wiki for more reasons.",
|
||||
"0300_0D00_0002_0006": "The build plate may not be properly placed. If this message appears repeatedly, please check the Wiki for more reasons.",
|
||||
"0300_0D00_0002_0007": "The build plate may not be properly placed. If this message appears repeatedly, please check the Wiki for more reasons.",
|
||||
"0300_0D00_0002_0008": "The build plate may not be properly placed. If this message appears repeatedly, please check the Wiki for more reasons.",
|
||||
"0300_0D00_0002_0009": "The build plate may not be properly placed. If this message appears repeatedly, please check the Wiki for more reasons.",
|
||||
"0300_0D00_0002_000A": "The build plate may not be properly placed. If this message appears repeatedly, please check the Wiki for more reasons.",
|
||||
"0300_0F00_0001_0001": "Abnormal accelerometer data detected. Please try to restart the printer.",
|
||||
"0300_1000_0002_0001": "The resonance frequency of the X axis is low. The timing belt may be loose.",
|
||||
"0300_1000_0002_0002": "The resonance frequency of the X axis differs greatly from last calibration. Please clean the carbon rod and rerun the machine calibration afterward.",
|
||||
"0300_1100_0002_0001": "The resonance frequency of the Y axis is low. The timing belt may be loose.",
|
||||
"0300_1100_0002_0002": "The resonance frequency of the Y axis differs greatly from the last calibration. Please clean the carbon rod and rerun the machine calibration afterward.",
|
||||
"0300_1200_0002_0001": "The front cover of the toolhead fell off.",
|
||||
"0300_1300_0001_0001": "The current sensor of Motor-A is abnormal. This may be caused by a failure of the hardware sampling circuit.",
|
||||
"0300_1400_0001_0001": "The current sensor of Motor-B is abnormal. This may be caused by a failure of the hardware sampling circuit.",
|
||||
"0300_1500_0001_0001": "The current sensor of Motor-Z is abnormal. This may be caused by a failure of the hardware sampling circuit.",
|
||||
"0300_1600_0001_0001": "The current sensor of Motor-E is abnormal. This may be caused by a failure of the hardware sampling circuit.",
|
||||
"0300_1700_0001_0001": "The hotend cooling fan speed is too slow or stopped. It may be stuck or the connector may not be plugged in properly.",
|
||||
"0300_1700_0002_0002": "The hotend cooling fan speed is slow. It may be stuck and need cleaning.",
|
||||
"0300_1800_0001_0001": "The value of extrusion force sensor is low, the nozzle seems to not be installed.",
|
||||
"0300_1800_0001_0002": "The sensitivity of the extrusion force sensor is low, the nozzle may not installed correctly.",
|
||||
"0300_1800_0001_0003": "The extrusion force sensor is not available, the link between the MC and TH may be broken or the sensor is broken.",
|
||||
"0300_1800_0001_0004": "The data from extrusion force sensor is abnormal, the sensor should be broken.",
|
||||
"0300_1800_0001_0005": "The Z axis motor got stuck while moving, or the extrusion force sensor may have an issue; please check if there is any foreign matter on the Z sliders or Z timing belt wheels.",
|
||||
"0300_1900_0001_0001": "The eddy current sensor on Y-axis is not available, the wire should be broken.",
|
||||
"0300_1900_0002_0002": "The sensitivity of Y-axis eddy current sensor is too low.",
|
||||
"0300_1A00_0002_0001": "The nozzle is covered with filaments, or the build plate is put in crooked.",
|
||||
"0300_1A00_0002_0002": "The nozzle is clogged with filament.",
|
||||
"0300_1B00_0001_0001": "The signal of the heatbed acceleration sensor is weak. The sensor may have fallen off or been damaged.",
|
||||
"0300_1B00_0001_0002": "External disturbance was detected on the heatbed acceleeration sensor. The sensor signal wire may not be fixed.",
|
||||
"0300_1B00_0001_0003": "The heatbed acceleration sensor detected unexpected continuous force. The sensor may be stuck, or the analog front end may be broken.",
|
||||
"0300_1C00_0001_0001": "The extrusion motor driver is abnormal. The MOSFET may have a short circuit.",
|
||||
"0300_1D00_0001_0001": "The position sensor of extrusion motor is abnormal. The connection to sensor may be loose.",
|
||||
"0300_2000_0001_0001": "X axis homing abnormal: please check if the tool head is stuck or the carbon rod has too much resistance.",
|
||||
"0300_2000_0001_0002": "Y axis homing abnormal: please check if the tool head is stuck or the Y carriage has too much resistance.",
|
||||
"0300_2000_0001_0003": "X axis homing abnormal: the timing belt may be loose.",
|
||||
"0300_2000_0001_0004": "Y axis homing abnormal: the timing belt may be loose.",
|
||||
"0300_4000_0002_0001": "Data transmission over the serial port is abnormal; the software system may be faulty.",
|
||||
"0300_4100_0001_0001": "The system voltage is unstable; triggering the power failure protection function.",
|
||||
"0300_9000_0001_0001": "Chamber heating failed. The chamber heater may be failing to blow hot air.",
|
||||
"0300_9000_0001_0002": "Chamber heating failed. The chamber may not be enclosed, or the ambient temperature may be too low, or the heat dissipation vent of the power supply may be blocked.",
|
||||
"0300_9000_0001_0003": "Chamber heating failed. The temperature of power supply may be too high.",
|
||||
"0300_9000_0001_0004": "Chamber heating failed. The speed of the heating fan is too low.",
|
||||
"0300_9000_0001_0005": "Chamber heating failed. The thermal resistance is too high.",
|
||||
"0300_9000_0001_0010": "The communication of chamber temperature controller is abnormal.",
|
||||
"0300_9100_0001_0001": "The temperature of chamber heater 1 is abnormal. The heater may have a short circuit.",
|
||||
"0300_9100_0001_0002": "The temperature of chamber heater 1 is abnormal. The heater may have an open circuit or the thermal fuse may have taken effect.",
|
||||
"0300_9100_0001_0003": "The temperature of chamber heater 1 is abnormal. The heater is over temperature.",
|
||||
"0300_9100_0001_0006": "The temperature of chamber heater 1 is abnormal. The sensor may have a short circuit.",
|
||||
"0300_9100_0001_0007": "The temperature of chamber heater 1 is abnormal. The sensor may have an open circuit.",
|
||||
"0300_9100_0001_0008": "Chamber heater 1 failed to rise to target temperature.",
|
||||
"0300_9100_0001_000A": "The temperature of chamber heater 1 is abnormal. The AC board may be broken.",
|
||||
"0300_9100_0001_000C": "The chamber heater 1 has been working at full load for a long time. The temperature control system may have an issue.",
|
||||
"0300_9200_0001_0001": "The temperature of chamber heater 2 is abnormal. The heater may have a short circuit.",
|
||||
"0300_9200_0001_0002": "The temperature of chamber heater 2 is abnormal. The heater may have an open circuit or the thermal fuse may be in effect.",
|
||||
"0300_9200_0001_0003": "The temperature of chamber heater 2 is abnormal. The heater is over temperature.",
|
||||
"0300_9200_0001_0006": "The temperature of chamber heater 2 is abnormal. The sensor may have a short circuit.",
|
||||
"0300_9200_0001_0007": "The temperature of chamber heater 2 is abnormal. The sensor may have an open circuit.",
|
||||
"0300_9200_0001_0008": "Chamber heater 2 failed to rise to target temperature.",
|
||||
"0300_9200_0001_000A": "The temperature of chamber heater 2 is abnormal. The AC board may be broken.",
|
||||
"0300_9300_0001_0001": "Chamber temperature is abnormal. The chamber heater's temperature sensor may have a short circuit.",
|
||||
"0300_9300_0001_0002": "Chamber temperature is abnormal. The chamber heater's temperature sensor may have an open circuit.",
|
||||
"0300_9300_0001_0003": "Chamber temperature is abnormal. The chamber heater's temperature sensor at the air outlet may have a short circuit.",
|
||||
"0300_9300_0001_0004": "Chamber temperature is abnormal. The chamber heater's temperature sensor at the air outlet may have an open circuit.",
|
||||
"0300_9300_0001_0005": "Chamber temperature is abnormal. The chamber heater's temperature sensor at the air inlet may have a short circuit.",
|
||||
"0300_9300_0001_0006": "Chamber temperature is abnormal. The chamber heater's temperature sensor at the air inlet may have an open circuit.",
|
||||
"0300_9300_0001_0007": "Chamber temperature is abnormal. The temperature sensor at the power supply may have a short circuit.",
|
||||
"0300_9300_0001_0008": "Chamber temperature is abnormal. The temperature sensor at power supply may have an open circuit.",
|
||||
"0300_9400_0002_0003": "Chamber failed to reach the desired temperature. The machine will stop waiting for the chamber temperature.",
|
||||
"0300_9400_0003_0001": "Chamber cooling may be too slow. You can open the front door or top cover to help cooling if the air in chamber is non-toxic.",
|
||||
"0300_9400_0003_0002": "Chamber temperature setting value exceed the limit, the boundary value will be set.",
|
||||
"0300_9500_0001_0007": "",
|
||||
"0300_9900_0001_0001": "",
|
||||
"0300_9900_0001_0002": "",
|
||||
"0300_9900_0001_0003": "",
|
||||
"0300_9900_0003_0001": "",
|
||||
"0500_0100_0002_0001": "The media pipeline is malfunctioning.",
|
||||
"0500_0100_0002_0002": "USB camera is not connected. Please check video camera cable connection.",
|
||||
"0500_0100_0002_0003": "USB camera is malfunctioning.",
|
||||
"0500_0100_0003_0004": "Not enough space in MicroSD Card; please clear some space.",
|
||||
"0500_0100_0003_0005": "MicroSD Card error: please reinsert, format or replace it.",
|
||||
"0500_0100_0003_0006": "Unformatted MicroSD Card: please format it.",
|
||||
"0500_0100_0003_0007": "Unable to record time-lapse photography without MicroSD card inserted.",
|
||||
"0500_0200_0002_0001": "Failed to connect internet. Please check the network connection.",
|
||||
"0500_0200_0002_0002": "Device login failed; please check your account information.",
|
||||
"0500_0200_0002_0003": "Failed to connect internet; please check the network connection.",
|
||||
"0500_0200_0002_0004": "Unauthorized user: please check your account information.",
|
||||
"0500_0200_0002_0005": "Failed to connect internet; please check the network connection.",
|
||||
"0500_0200_0002_0006": "Streaming function error. Please check the network and try again. You can restart or update the printer if the issue persists.",
|
||||
"0500_0200_0002_0007": "Liveview service login failed; please check your network connection.",
|
||||
"0500_0200_0002_0008": "Time synchronization failed.",
|
||||
"0500_0300_0001_0001": "The MC module is malfunctioning; please restart the device or check device cable connection.",
|
||||
"0500_0300_0001_0002": "The toolhead is malfunctioning. Please restart the device.",
|
||||
"0500_0300_0001_0003": "The AMS module is malfunctioning. Please restart the device.",
|
||||
"0500_0300_0001_0004": "The AHB module is malfunctioning. Please restart the device.",
|
||||
"0500_0300_0001_0005": "Internal service is malfunctioning. Please restart the device.",
|
||||
"0500_0300_0001_0006": "A system panic occurred. Please restart the device.",
|
||||
"0500_0300_0001_0008": "A system hang occurred. Please restart the device.",
|
||||
"0500_0300_0001_0009": "A system hang occurred. It has been recovered by automatic restart.",
|
||||
"0500_0300_0001_000A": "System state is abnormal; please restore factory settings.",
|
||||
"0500_0300_0001_000B": "The screen is malfunctioning; please restart the device.",
|
||||
"0500_0300_0001_0021": "Hardware incompatible: please check the laser.",
|
||||
"0500_0300_0001_0023": "The Chamber Temperature Control module is malfunctioning. Please restart the device.",
|
||||
"0500_0300_0001_0024": "The current temperature is too low. In order to protect you and your printer. Printing task, moving axis and other operations is disabled. Please move the printer to an environment above 10°C.",
|
||||
"0500_0300_0001_0025": "The current firmware is abnormal. Please update again.",
|
||||
"0500_0300_0002_000C": "Wireless hardware error: please turn off/on WiFi or restart the device.",
|
||||
"0500_0300_0002_000D": "The SD Card controller is malfunctioning.",
|
||||
"0500_0300_0002_0010": "forward coredump, it is recovering.",
|
||||
"0500_0300_0002_0011": "upgrade coredump, it is recovering.",
|
||||
"0500_0300_0002_0012": "ipcam coredump, it is recovering.",
|
||||
"0500_0300_0002_0013": "xcam coredump, it is recovering.",
|
||||
"0500_0300_0002_0014": "bbl_screen coredump, it is recovering.",
|
||||
"0500_0300_0002_0015": "device_gate coredump, it is recovering.",
|
||||
"0500_0300_0002_0016": "device_manager coredump, it is recovering.",
|
||||
"0500_0300_0002_0017": "recorder coredump, it is recovering.",
|
||||
"0500_0300_0002_0018": "security coredump, it is recovering.",
|
||||
"0500_0300_0002_0020": "Micro SD Card capacity is insufficient to cache print files.",
|
||||
"0500_0300_0003_0007": "A system panic occurred. It has been recovered by automatic restart.",
|
||||
"0500_0300_0003_0022": "MicroSD Card performance degradation has been detected. It may affect print jobs, logs, and video records. Please try to format or change the MicroSD card.",
|
||||
"0500_0400_0001_0001": "Failed to download print job. Please check your network connection.",
|
||||
"0500_0400_0001_0002": "Failed to report print state. Please check your network connection.",
|
||||
"0500_0400_0001_0003": "The content of print file is unreadable. Please resend the print job.",
|
||||
"0500_0400_0001_0004": "The print file is unauthorized.",
|
||||
"0500_0400_0001_0006": "Failed to resume previous print.",
|
||||
"0500_0400_0002_0007": "The bed temperature exceeds the filament's vitrification temperature, which may cause a nozzle clog. Please keep the front door of the printer open or lower the bed temperature.",
|
||||
"0500_0400_0002_0010": "The RFID-tag on AMS A Slot1 cannot be identified.",
|
||||
"0500_0400_0002_0011": "The RFID-tag on AMS A Slot2 cannot be identified.",
|
||||
"0500_0400_0002_0012": "The RFID-tag on AMS A Slot3 cannot be identified.",
|
||||
"0500_0400_0002_0013": "The RFID-tag on AMS A Slot4 cannot be identified.",
|
||||
"0500_0400_0002_0014": "The RFID-tag on AMS B Slot1 cannot be identified.",
|
||||
"0500_0400_0002_0015": "The RFID-tag on AMS B Slot2 cannot be identified.",
|
||||
"0500_0400_0002_0016": "The RFID-tag on AMS B Slot3 cannot be identified.",
|
||||
"0500_0400_0002_0017": "The RFID-tag on AMS B Slot4 cannot be identified.",
|
||||
"0500_0400_0002_0018": "The RFID-tag on AMS C Slot1 cannot be identified.",
|
||||
"0500_0400_0002_0019": "The RFID-tag on AMS C Slot2 cannot be identified.",
|
||||
"0500_0400_0002_001A": "The RFID-tag on AMS C Slot3 cannot be identified.",
|
||||
"0500_0400_0002_001B": "The RFID-tag on AMS C Slot4 cannot be identified.",
|
||||
"0500_0400_0002_001C": "The RFID-tag on AMS D Slot1 cannot be identified.",
|
||||
"0500_0400_0002_001D": "The RFID-tag on AMS D Slot2 cannot be identified.",
|
||||
"0500_0400_0002_001E": "The RFID-tag on AMS D Slot3 cannot be identified.",
|
||||
"0500_0400_0002_001F": "The RFID-tag on AMS D Slot4 cannot be identified.",
|
||||
"0500_0400_0002_0020": "",
|
||||
"0500_0400_0003_0008": "The door is detected to be open.",
|
||||
"0500_0400_0003_0009": "The bed temperature exceeds filament's vitrification temperature, which may cause nozzle clog. Please keep the front door of printer open. Already turning off the door opening detection temporarily.",
|
||||
"0500_0500_0001_0001": "The factory data of AP board is abnormal; please replace the AP board with a new one.",
|
||||
"0500_0500_0001_0006": "The factory data of AP board is abnormal, please replace the AP board with a new one.",
|
||||
"0500_0500_0003_0002": "The device is in the engineering state; please pay attention to the information security related matters.",
|
||||
"07FF_2000_0002_0001": "External filament has run out; please load a new filament.",
|
||||
"07FF_2000_0002_0002": "External filament is missing; please load a new filament.",
|
||||
"07FF_2000_0002_0004": "Please pull the external filament from the extruder.",
|
||||
"0C00_0100_0001_0001": "The Micro Lidar camera is offline. Please check the hardware connection.",
|
||||
"0C00_0100_0001_0003": "Synchronization between the Micro Lidar camera and MCU is abnormal. Please restart your printer.",
|
||||
"0C00_0100_0001_0004": "The Micro Lidar camera lens seems to be dirty. Please clean the lens.",
|
||||
"0C00_0100_0001_0005": "Micro Lidar camera OTP parameter is abnormal. Please contact after-sales.",
|
||||
"0C00_0100_0001_0009": "The chamber camera lens seems to be dirty. Please clean the lens.",
|
||||
"0C00_0100_0001_000A": "The Micro Lidar LED may be broken.",
|
||||
"0C00_0100_0001_000B": "Failed to calibrate Micro Lidar. Please make sure the calibration chart is clean and not occluded, and run machine calibration again.",
|
||||
"0C00_0100_0002_0002": "The Micro Lidar camera is malfunctioning and related functions may fail. Please contact after-sales if this message keeps appearing in multiple prints.",
|
||||
"0C00_0100_0002_0006": "Micro Lidar camera extrinsic parameters are abnormal. Please enable flowrate calibration in your next print.",
|
||||
"0C00_0100_0002_0007": "Micro Lidar laser parameters are drifted. Please re-calibrate your printer.",
|
||||
"0C00_0100_0002_0008": "Failed to get image from chamber camera. Spaghetti and waste chute pileup detection is not available for now.",
|
||||
"0C00_0200_0001_0001": "The horizontal laser is not lit. Please check if it's covered or hardware connection is normal.",
|
||||
"0C00_0200_0001_0005": "A new Micro Lidar was detected. Please calibrate it on Calibration page before use.",
|
||||
"0C00_0200_0002_0002": "The horizontal laser line is too wide. Please check if the heatbed is dirty.",
|
||||
"0C00_0200_0002_0003": "The horizontal laser is not bright enough at homing position. Please clean or replace heatbed if this message appears repeatedly.",
|
||||
"0C00_0200_0002_0004": "Nozzle height seems too low. Please check if the nozzle is worn or tilted. Re-calibrate Lidar if the nozzle has been replaced.",
|
||||
"0C00_0200_0002_0006": "Nozzle height seems too high. Please check if there is filament residual attached to the nozzle.",
|
||||
"0C00_0200_0002_0007": "The vertical laser is not lit. Please check if it's covered or hardware connection is normal.",
|
||||
"0C00_0200_0002_0008": "The vertical laser line is too wide. Please check if the heatbed is dirty.",
|
||||
"0C00_0200_0002_0009": "The vertical laser is not bright enough at homing position. Please clean or replace heatbed if this message appears repeatedly.",
|
||||
"0C00_0300_0001_0009": "The first layer inspection module rebooted abnormally. The inspection result may be inaccurate.",
|
||||
"0C00_0300_0001_000A": "Your printer is in factory mode. Please contact Technical Support.",
|
||||
"0C00_0300_0002_0001": "Filament exposure metering failed because laser reflection is too weak on this material. First layer inspection may be inaccurate.",
|
||||
"0C00_0300_0002_0002": "First layer inspection terminated due to abnormal Lidar data.",
|
||||
"0C00_0300_0002_0004": "First layer inspection is not supported for the current print job.",
|
||||
"0C00_0300_0002_0005": "First layer inspection timed out abnormally, and the current results may be inaccurate.",
|
||||
"0C00_0300_0002_000C": "The build plate localization marker is not detected. Please check if the build plate is aligned correctly.",
|
||||
"0C00_0300_0002_000F": "Parts skipped before first layer inspection; the inspection will not be supported for the current print.",
|
||||
"0C00_0300_0002_0010": "Foreign objects detected on heatbed, Please check and clean the heatbed.",
|
||||
"0C00_0300_0003_0006": "Purged filament may have piled up in the waste chute. Please check and clean the chute.",
|
||||
"0C00_0300_0003_0007": "Possible first layer defects have been detected. Please check the first layer quality and decide if the job should be stopped.",
|
||||
"0C00_0300_0003_0008": "Possible spaghetti defects were detected. Please check the print quality and decide if the job should be stopped.",
|
||||
"0C00_0300_0003_000B": "Inspecting the first layer: please wait a moment.",
|
||||
"0C00_0300_0003_000D": "Some objects may have fallen down, or the extruder is not extruding normally. Please check and decide if the printing should be stopped.",
|
||||
"0C00_0300_0003_000E": "Your nozzle seems to be covered with jammed or clogged material.",
|
||||
"0C00_0300_0003_000F": "Your nozzle seems to be covered with jammed or clogged material.",
|
||||
"0C00_0300_0003_0010": "Your printer seems to be printing without extruding.",
|
||||
"0C00_0400_0001_0017": "",
|
||||
"1200_1000_0001_0001": "The AMS A Slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"1200_1000_0001_0003": "The AMS A Slot1 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"1200_1000_0002_0002": "The AMS A Slot1 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"1200_1100_0001_0001": "The AMS A Slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"1200_1100_0001_0003": "The AMS A Slot2 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"1200_1100_0002_0002": "The AMS A Slot2 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"1200_1200_0001_0001": "The AMS A Slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"1200_1200_0001_0003": "The AMS A Slot3 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"1200_1200_0002_0002": "The AMS A Slot3 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"1200_1300_0001_0001": "The AMS A Slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"1200_1300_0001_0003": "The AMS A Slot4 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"1200_1300_0002_0002": "The AMS A Slot4 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"1200_2000_0002_0001": "AMS A Slot1 filament has run out; please insert a new filament.",
|
||||
"1200_2000_0002_0002": "AMS A Slot 1 is empty; please insert a new filament.",
|
||||
"1200_2000_0002_0003": "AMS A Slot1 filament may be broken in the PTFE tube.",
|
||||
"1200_2000_0002_0004": "AMS A Slot1 filament may be broken in the tool head.",
|
||||
"1200_2000_0002_0005": "AMS A Slot1 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
|
||||
"1200_2000_0002_0006": "Failed to extrude AMS A Slot1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
|
||||
"1200_2000_0003_0001": "AMS A Slot1 filament has run out. Purging the old filament; please wait.",
|
||||
"1200_2000_0003_0002": "AMS A Slot1 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"1200_2100_0002_0001": "AMS A Slot2 filament has run out; please insert a new filament.",
|
||||
"1200_2100_0002_0002": "AMS A Slot 2 is empty; please insert a new filament.",
|
||||
"1200_2100_0002_0003": "AMS A Slot2 filament may be broken in the PTFE tube.",
|
||||
"1200_2100_0002_0004": "AMS A Slot2 filament may be broken in the tool head.",
|
||||
"1200_2100_0002_0005": "AMS A Slot2 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
|
||||
"1200_2100_0002_0006": "Failed to extrude AMS A Slot2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
|
||||
"1200_2100_0003_0001": "AMS A Slot2 filament has run out. Purging the old filament; please wait.",
|
||||
"1200_2100_0003_0002": "AMS A Slot2 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"1200_2200_0002_0001": "AMS A Slot3 filament has run out; please insert a new filament.",
|
||||
"1200_2200_0002_0002": "AMS A Slot 3 is empty; please insert a new filament.",
|
||||
"1200_2200_0002_0003": "AMS A Slot3 filament may be broken in the PTFE tube.",
|
||||
"1200_2200_0002_0004": "AMS A Slot3 filament may be broken in the tool head.",
|
||||
"1200_2200_0002_0005": "AMS A Slot3 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
|
||||
"1200_2200_0002_0006": "Failed to extrude AMS A Slot3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
|
||||
"1200_2200_0003_0001": "AMS A Slot3 filament has run out. Purging the old filament; please wait.",
|
||||
"1200_2200_0003_0002": "AMS A Slot3 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"1200_2300_0002_0001": "AMS A Slot4 filament has run out; please insert a new filament.",
|
||||
"1200_2300_0002_0002": "AMS A Slot 4 is empty; please insert a new filament.",
|
||||
"1200_2300_0002_0003": "AMS A Slot4 filament may be broken in the PTFE tube.",
|
||||
"1200_2300_0002_0004": "AMS A Slot4 filament may be broken in the tool head.",
|
||||
"1200_2300_0002_0005": "AMS A Slot4 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
|
||||
"1200_2300_0002_0006": "Failed to extrude AMS A Slot4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
|
||||
"1200_2300_0003_0001": "AMS A Slot4 filament has run out. Purging the old filament; please wait.",
|
||||
"1200_2300_0003_0002": "AMS A Slot4 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"1200_2400_0002_0001": "Filament may be broken in the tool head.",
|
||||
"1200_2500_0002_0001": "Failed to extrude the filament and the extruder may be clogged.",
|
||||
"1200_3000_0001_0001": "AMS A Slot 1 RFID coil is broken or the RF hardware circuit has an error.",
|
||||
"1200_3000_0001_0004": "RFID cannot be read because of an encryption chip failure in AMSA.",
|
||||
"1200_3000_0002_0002": "The RFID-tag on AMS A Slot 1 is damaged.",
|
||||
"1200_3000_0003_0003": "AMS A Slot1 RFID cannot be read because of a structural error.",
|
||||
"1200_3100_0001_0001": "AMS A Slot 2 RFID coil is broken or the RF hardware circuit has an error.",
|
||||
"1200_3100_0002_0002": "The RFID-tag on AMS A Slot 2 is damaged.",
|
||||
"1200_3100_0003_0003": "AMS A Slot2 RFID cannot be read because of a structural error.",
|
||||
"1200_3200_0001_0001": "AMS A Slot 3 RFID coil is broken or the RF hardware circuit has an error.",
|
||||
"1200_3200_0002_0002": "The RFID-tag on AMS A Slot 3 is damaged.",
|
||||
"1200_3200_0003_0003": "AMS A Slot3 RFID cannot be read because of a structural error.",
|
||||
"1200_3300_0001_0001": "AMS A Slot 4 RFID coil is broken or the RF hardware circuit has an error.",
|
||||
"1200_3300_0002_0002": "The RFID-tag on AMS A Slot 4 is damaged.",
|
||||
"1200_3300_0003_0003": "AMS A Slot4 RFID cannot be read because of a structural error.",
|
||||
"1200_4500_0002_0001": "The filament cutter sensor is malfunctioning. Please check whether the connector is properly plugged in.",
|
||||
"1200_4500_0002_0002": "The filament cutter's cutting distance is too large. The X motor may lose steps.",
|
||||
"1200_4500_0002_0003": "The filament cutter handle has not been released. The handle or blade may be jammed, or there could be an issue with the filament sensor connection.",
|
||||
"1200_5000_0002_0001": "AMS A communication is abnormal; please check the connection cable.",
|
||||
"1200_5100_0003_0001": "AMS is disabled; please load filament from spool holder.",
|
||||
"1200_7000_0001_0001": "AMS A Filament speed and length error: The slot 1 filament odometry may be faulty.",
|
||||
"1200_7100_0001_0001": "AMS A Filament speed and length error: The slot 2 filament odometry may be faulty.",
|
||||
"1200_7200_0001_0001": "AMS A Filament speed and length error: The slot 3 filament odometry may be faulty.",
|
||||
"1200_7300_0001_0001": "AMS A Filament speed and length error: The slot 4 filament odometry may be faulty.",
|
||||
"1200_8000_0002_0001": "AMS A Slot1 filament may be tangled or stuck.",
|
||||
"1200_8100_0002_0001": "AMS A Slot2 filament may be tangled or stuck.",
|
||||
"1200_8200_0002_0001": "AMS A Slot3 filament may be tangled or stuck.",
|
||||
"1200_8300_0002_0001": "AMS A Slot4 filament may be tangled or stuck.",
|
||||
"1201_1000_0001_0001": "The AMS B Slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"1201_1000_0001_0003": "The AMS B Slot1 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"1201_1000_0002_0002": "The AMS B Slot1 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"1201_1100_0001_0001": "The AMS B Slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"1201_1100_0001_0003": "The AMS B Slot2 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"1201_1100_0002_0002": "The AMS B Slot2 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"1201_1200_0001_0001": "The AMS B Slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"1201_1200_0001_0003": "The AMS B Slot3 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"1201_1200_0002_0002": "The AMS B Slot3 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"1201_1300_0001_0001": "The AMS B Slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"1201_1300_0001_0003": "The AMS B Slot4 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"1201_1300_0002_0002": "The AMS B Slot4 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"1201_2000_0002_0001": "AMS B Slot1 filament has run out; please insert a new filament.",
|
||||
"1201_2000_0002_0002": "AMS B Slot 1 is empty; please insert a new filament.",
|
||||
"1201_2000_0002_0003": "AMS B Slot1 filament may be broken in the PTFE tube.",
|
||||
"1201_2000_0002_0004": "AMS B Slot1 filament may be broken in the tool head.",
|
||||
"1201_2000_0002_0005": "AMS B Slot1 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
|
||||
"1201_2000_0002_0006": "Failed to extrude AMS B Slot1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
|
||||
"1201_2000_0003_0001": "AMS B Slot1 filament has run out. Purging the old filament; please wait.",
|
||||
"1201_2000_0003_0002": "AMS B Slot1 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"1201_2100_0002_0001": "AMS B Slot2 filament has run out; please insert a new filament.",
|
||||
"1201_2100_0002_0002": "AMS B Slot 2 is empty; please insert a new filament.",
|
||||
"1201_2100_0002_0003": "AMS B Slot2 filament may be broken in the PTFE tube.",
|
||||
"1201_2100_0002_0004": "AMS B Slot2 filament may be broken in the tool head.",
|
||||
"1201_2100_0002_0005": "AMS B Slot2 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
|
||||
"1201_2100_0002_0006": "Failed to extrude AMS B Slot2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
|
||||
"1201_2100_0003_0001": "AMS B Slot2 filament has run out. Purging the old filament; please wait.",
|
||||
"1201_2100_0003_0002": "AMS B Slot2 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"1201_2200_0002_0001": "AMS B Slot3 filament has run out; please insert a new filament.",
|
||||
"1201_2200_0002_0002": "AMS B Slot 3 is empty; please insert a new filament.",
|
||||
"1201_2200_0002_0003": "AMS B Slot3 filament may be broken in the PTFE tube.",
|
||||
"1201_2200_0002_0004": "AMS B Slot3 filament may be broken in the tool head.",
|
||||
"1201_2200_0002_0005": "AMS B Slot3 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
|
||||
"1201_2200_0002_0006": "Failed to extrude AMS B Slot3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
|
||||
"1201_2200_0003_0001": "AMS B Slot3 filament has run out. Purging the old filament; please wait.",
|
||||
"1201_2200_0003_0002": "AMS B Slot3 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"1201_2300_0002_0001": "AMS B Slot4 filament has run out; please insert a new filament.",
|
||||
"1201_2300_0002_0002": "AMS B Slot 4 is empty; please insert a new filament.",
|
||||
"1201_2300_0002_0003": "AMS B Slot4 filament may be broken in the PTFE tube.",
|
||||
"1201_2300_0002_0004": "AMS B Slot4 filament may be broken in the tool head.",
|
||||
"1201_2300_0002_0005": "AMS B Slot4 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
|
||||
"1201_2300_0002_0006": "Failed to extrude AMS B Slot4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
|
||||
"1201_2300_0003_0001": "AMS B Slot4 filament has run out. Purging the old filament; please wait.",
|
||||
"1201_2300_0003_0002": "AMS B Slot4 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"1201_3000_0001_0001": "AMS B Slot 1 RFID coil is broken or the RF hardware circuit has an error.",
|
||||
"1201_3000_0001_0004": "RFID cannot be read because of an encryption chip failure in AMSB.",
|
||||
"1201_3000_0002_0002": "The RFID-tag on AMS B Slot 1 is damaged.",
|
||||
"1201_3000_0003_0003": "AMS B Slot1 RFID cannot be read because of a structural error.",
|
||||
"1201_3100_0001_0001": "AMS B Slot 2 RFID coil is broken or the RF hardware circuit has an error.",
|
||||
"1201_3100_0002_0002": "The RFID-tag on AMS B Slot 2 is damaged.",
|
||||
"1201_3100_0003_0003": "AMS B Slot2 RFID cannot be read because of a structural error.",
|
||||
"1201_3200_0001_0001": "AMS B Slot 3 RFID coil is broken or the RF hardware circuit has an error.",
|
||||
"1201_3200_0002_0002": "The RFID-tag on AMS B Slot 3 is damaged.",
|
||||
"1201_3200_0003_0003": "AMS B Slot3 RFID cannot be read because of a structural error.",
|
||||
"1201_3300_0001_0001": "AMS B Slot 4 RFID coil is broken or the RF hardware circuit has an error.",
|
||||
"1201_3300_0002_0002": "The RFID-tag on AMS B Slot 4 is damaged.",
|
||||
"1201_3300_0003_0003": "AMS B Slot4 RFID cannot be read because of a structural error.",
|
||||
"1201_5000_0002_0001": "AMS B communication is abnormal; please check the connection cable.",
|
||||
"1201_7000_0001_0001": "AMS B Filament speed and length error: The slot 1 filament odometry may be faulty.",
|
||||
"1201_7100_0001_0001": "AMS B Filament speed and length error: The slot 2 filament odometry may be faulty.",
|
||||
"1201_7200_0001_0001": "AMS B Filament speed and length error: The slot 3 filament odometry may be faulty.",
|
||||
"1201_7300_0001_0001": "AMS B Filament speed and length error: The slot 4 filament odometry may be faulty.",
|
||||
"1201_8000_0002_0001": "AMS B Slot1 filament may be tangled or stuck.",
|
||||
"1201_8100_0002_0001": "AMS B Slot2 filament may be tangled or stuck.",
|
||||
"1201_8200_0002_0001": "AMS B Slot3 filament may be tangled or stuck.",
|
||||
"1201_8300_0002_0001": "AMS B Slot4 filament may be tangled or stuck.",
|
||||
"1202_1000_0001_0001": "The AMS C Slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"1202_1000_0001_0003": "The AMS C Slot1 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"1202_1000_0002_0002": "The AMS C Slot1 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"1202_1100_0001_0001": "The AMS C Slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"1202_1100_0001_0003": "The AMS C Slot2 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"1202_1100_0002_0002": "The AMS C Slot2 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"1202_1200_0001_0001": "The AMS C Slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"1202_1200_0001_0003": "The AMS C Slot3 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"1202_1200_0002_0002": "The AMS C Slot3 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"1202_1300_0001_0001": "The AMS C Slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"1202_1300_0001_0003": "The AMS C Slot4 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"1202_1300_0002_0002": "The AMS C Slot4 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"1202_2000_0002_0001": "AMS C Slot1 filament has run out; please insert a new filament.",
|
||||
"1202_2000_0002_0002": "AMS C Slot 1 is empty; please insert a new filament.",
|
||||
"1202_2000_0002_0003": "AMS C Slot1 filament may be broken in the PTFE tube.",
|
||||
"1202_2000_0002_0004": "AMS C Slot1 filament may be broken in the tool head.",
|
||||
"1202_2000_0002_0005": "AMS C Slot1 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
|
||||
"1202_2000_0002_0006": "Failed to extrude AMS C Slot1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
|
||||
"1202_2000_0003_0001": "AMS C Slot1 filament has run out. Purging the old filament; please wait.",
|
||||
"1202_2000_0003_0002": "AMS C Slot1 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"1202_2100_0002_0001": "AMS C Slot2 filament has run out; please insert a new filament.",
|
||||
"1202_2100_0002_0002": "AMS C Slot 2 is empty; please insert a new filament.",
|
||||
"1202_2100_0002_0003": "AMS C Slot2 filament may be broken in the PTFE tube.",
|
||||
"1202_2100_0002_0004": "AMS C Slot2 filament may be broken in the tool head.",
|
||||
"1202_2100_0002_0005": "AMS C Slot2 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
|
||||
"1202_2100_0002_0006": "Failed to extrude AMS C Slot2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
|
||||
"1202_2100_0003_0001": "AMS C Slot2 filament has run out. Purging the old filament; please wait.",
|
||||
"1202_2100_0003_0002": "AMS C Slot2 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"1202_2200_0002_0001": "AMS C Slot3 filament has run out; please insert a new filament.",
|
||||
"1202_2200_0002_0002": "AMS C Slot 3 is empty; please insert a new filament.",
|
||||
"1202_2200_0002_0003": "AMS C Slot3 filament may be broken in the PTFE tube.",
|
||||
"1202_2200_0002_0004": "AMS C Slot3 filament may be broken in the tool head.",
|
||||
"1202_2200_0002_0005": "AMS C Slot3 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
|
||||
"1202_2200_0002_0006": "Failed to extrude AMS C Slot3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
|
||||
"1202_2200_0003_0001": "AMS C Slot3 filament has run out. Purging the old filament; please wait.",
|
||||
"1202_2200_0003_0002": "AMS C Slot3 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"1202_2300_0002_0001": "AMS C Slot4 filament has run out; please insert a new filament.",
|
||||
"1202_2300_0002_0002": "AMS C Slot 4 is empty; please insert a new filament.",
|
||||
"1202_2300_0002_0003": "AMS C Slot4 filament may be broken in the PTFE tube.",
|
||||
"1202_2300_0002_0004": "AMS C Slot4 filament may be broken in the tool head.",
|
||||
"1202_2300_0002_0005": "AMS C Slot4 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
|
||||
"1202_2300_0002_0006": "Failed to extrude AMS C Slot4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
|
||||
"1202_2300_0003_0001": "AMS C Slot4 filament has run out. Purging the old filament; please wait.",
|
||||
"1202_2300_0003_0002": "AMS C Slot4 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"1202_3000_0001_0001": "AMS C Slot 1 RFID coil is broken or the RF hardware circuit has an error.",
|
||||
"1202_3000_0001_0004": "RFID cannot be read because of an encryption chip failure in AMSC.",
|
||||
"1202_3000_0002_0002": "The RFID-tag on AMS C Slot 1 is damaged.",
|
||||
"1202_3000_0003_0003": "AMS C Slot1 RFID cannot be read because of a structural error.",
|
||||
"1202_3100_0001_0001": "AMS C Slot 2 RFID coil is broken or the RF hardware circuit has an error.",
|
||||
"1202_3100_0002_0002": "The RFID-tag on AMS C Slot 2 is damaged.",
|
||||
"1202_3100_0003_0003": "AMS C Slot2 RFID cannot be read because of a structural error.",
|
||||
"1202_3200_0001_0001": "AMS C Slot 3 RFID coil is broken or the RF hardware circuit has an error.",
|
||||
"1202_3200_0002_0002": "The RFID-tag on AMS C Slot 3 is damaged.",
|
||||
"1202_3200_0003_0003": "AMS C Slot3 RFID cannot be read because of a structural error.",
|
||||
"1202_3300_0001_0001": "AMS C Slot 4 RFID coil is broken or the RF hardware circuit has an error.",
|
||||
"1202_3300_0002_0002": "The RFID-tag on AMS C Slot 4 is damaged.",
|
||||
"1202_3300_0003_0003": "AMS C Slot4 RFID cannot be read because of a structural error.",
|
||||
"1202_5000_0002_0001": "AMS C communication is abnormal; please check the connection cable.",
|
||||
"1202_7000_0001_0001": "AMS C Filament speed and length error: The slot 1 filament odometry may be faulty.",
|
||||
"1202_7100_0001_0001": "AMS C Filament speed and length error: The slot 2 filament odometry may be faulty.",
|
||||
"1202_7200_0001_0001": "AMS C Filament speed and length error: The slot 3 filament odometry may be faulty.",
|
||||
"1202_7300_0001_0001": "AMS C Filament speed and length error: The slot 4 filament odometry may be faulty.",
|
||||
"1202_8000_0002_0001": "AMS C Slot1 filament may be tangled or stuck.",
|
||||
"1202_8100_0002_0001": "AMS C Slot2 filament may be tangled or stuck.",
|
||||
"1202_8200_0002_0001": "AMS C Slot3 filament may be tangled or stuck.",
|
||||
"1202_8300_0002_0001": "AMS C Slot4 filament may be tangled or stuck.",
|
||||
"1203_1000_0001_0001": "The AMS D Slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"1203_1000_0001_0003": "The AMS D Slot1 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"1203_1000_0002_0002": "The AMS D Slot1 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"1203_1100_0001_0001": "The AMS D Slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"1203_1100_0001_0003": "The AMS D Slot2 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"1203_1100_0002_0002": "The AMS D Slot2 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"1203_1200_0001_0001": "The AMS D Slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"1203_1200_0001_0003": "The AMS D Slot3 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"1203_1200_0002_0002": "The AMS D Slot3 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"1203_1300_0001_0001": "The AMS D Slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
|
||||
"1203_1300_0001_0003": "The AMS D Slot4 motor torque control is malfunctioning. The current sensor may be faulty.",
|
||||
"1203_1300_0002_0002": "The AMS D Slot4 motor is overloaded. The filament may be tangled or stuck.",
|
||||
"1203_2000_0002_0001": "AMS D Slot1 filament has run out; please insert a new filament.",
|
||||
"1203_2000_0002_0002": "AMS D Slot 1 is empty; please insert a new filament.",
|
||||
"1203_2000_0002_0003": "AMS D Slot1 filament may be broken in the PTFE tube.",
|
||||
"1203_2000_0002_0004": "AMS D Slot1 filament may be broken in the tool head.",
|
||||
"1203_2000_0002_0005": "AMS D Slot1 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
|
||||
"1203_2000_0002_0006": "Failed to extrude AMS D Slot1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
|
||||
"1203_2000_0003_0001": "AMS D Slot1 filament has run out. Purging the old filament; please wait.",
|
||||
"1203_2000_0003_0002": "AMS D Slot1 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"1203_2100_0002_0001": "AMS D Slot2 filament has run out; please insert a new filament.",
|
||||
"1203_2100_0002_0002": "AMS D Slot 2 is empty; please insert a new filament.",
|
||||
"1203_2100_0002_0003": "AMS D Slot2 filament may be broken in the PTFE tube.",
|
||||
"1203_2100_0002_0004": "AMS D Slot2 filament may be broken in the tool head.",
|
||||
"1203_2100_0002_0005": "AMS D Slot2 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
|
||||
"1203_2100_0002_0006": "Failed to extrude AMS D Slot2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
|
||||
"1203_2100_0003_0001": "AMS D Slot2 filament has run out. Purging the old filament; please wait.",
|
||||
"1203_2100_0003_0002": "AMS D Slot2 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"1203_2200_0002_0001": "AMS D Slot3 filament has run out; please insert a new filament.",
|
||||
"1203_2200_0002_0002": "AMS D Slot 3 is empty; please insert a new filament.",
|
||||
"1203_2200_0002_0003": "AMS D Slot3 filament may be broken in the PTFE tube.",
|
||||
"1203_2200_0002_0004": "AMS D Slot3 filament may be broken in the tool head.",
|
||||
"1203_2200_0002_0005": "AMS D Slot3 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
|
||||
"1203_2200_0002_0006": "Failed to extrude AMS D Slot3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
|
||||
"1203_2200_0003_0001": "AMS D Slot3 filament has run out. Purging the old filament; please wait.",
|
||||
"1203_2200_0003_0002": "AMS D Slot3 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"1203_2300_0002_0001": "AMS D Slot4 filament has run out; please insert a new filament.",
|
||||
"1203_2300_0002_0002": "AMS D Slot 4 is empty; please insert a new filament.",
|
||||
"1203_2300_0002_0003": "AMS D Slot4 filament may be broken in the PTFE tube.",
|
||||
"1203_2300_0002_0004": "AMS D Slot4 filament may be broken in the tool head.",
|
||||
"1203_2300_0002_0005": "AMS D Slot4 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
|
||||
"1203_2300_0002_0006": "Failed to extrude AMS D Slot4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
|
||||
"1203_2300_0003_0001": "AMS D Slot4 filament has run out. Purging the old filament; please wait.",
|
||||
"1203_2300_0003_0002": "AMS D Slot4 filament has run out and automatically switched to the slot with the same filament.",
|
||||
"1203_3000_0001_0001": "AMS D Slot 1 RFID coil is broken or the RF hardware circuit has an error.",
|
||||
"1203_3000_0001_0004": "RFID cannot be read because of an encryption chip failure in AMSD.",
|
||||
"1203_3000_0002_0002": "The RFID-tag on AMS D Slot 1 is damaged.",
|
||||
"1203_3000_0003_0003": "AMS D Slot1 RFID cannot be read because of a structural error.",
|
||||
"1203_3100_0001_0001": "AMS D Slot 2 RFID coil is broken or the RF hardware circuit has an error.",
|
||||
"1203_3100_0002_0002": "The RFID-tag on AMS D Slot 2 is damaged.",
|
||||
"1203_3100_0003_0003": "AMS D Slot2 RFID cannot be read because of a structural error.",
|
||||
"1203_3200_0001_0001": "AMS D Slot 3 RFID coil is broken or the RF hardware circuit has an error.",
|
||||
"1203_3200_0002_0002": "The RFID-tag on AMS D Slot 3 is damaged.",
|
||||
"1203_3200_0003_0003": "AMS D Slot3 RFID cannot be read because of a structural error.",
|
||||
"1203_3300_0001_0001": "AMS D Slot 4 RFID coil is broken or the RF hardware circuit has an error.",
|
||||
"1203_3300_0002_0002": "The RFID-tag on AMS D Slot 4 is damaged.",
|
||||
"1203_3300_0003_0003": "AMS D Slot4 RFID cannot be read because of a structural error.",
|
||||
"1203_5000_0002_0001": "AMS D communication is abnormal; please check the connection cable.",
|
||||
"1203_7000_0001_0001": "AMS D Filament speed and length error: The slot 1 filament odometry may be faulty.",
|
||||
"1203_7100_0001_0001": "AMS D Filament speed and length error: The slot 2 filament odometry may be faulty.",
|
||||
"1203_7200_0001_0001": "AMS D Filament speed and length error: The slot 3 filament odometry may be faulty.",
|
||||
"1203_7300_0001_0001": "AMS D Filament speed and length error: The slot 4 filament odometry may be faulty.",
|
||||
"1203_8000_0002_0001": "AMS D Slot1 filament may be tangled or stuck.",
|
||||
"1203_8100_0002_0001": "AMS D Slot2 filament may be tangled or stuck.",
|
||||
"1203_8200_0002_0001": "AMS D Slot3 filament may be tangled or stuck.",
|
||||
"1203_8300_0002_0001": "AMS D Slot4 filament may be tangled or stuck.",
|
||||
"12FF_2000_0002_0001": "Filament at the spool holder has run out; please insert a new filament.",
|
||||
"12FF_2000_0002_0002": "Filament on the spool holder is empty; please insert a new filament.",
|
||||
"12FF_2000_0002_0004": "Please pull out the filament on the spool holder from the extruder.",
|
||||
"12FF_2000_0002_0005": "Filament may be broken in the tool head.",
|
||||
"12FF_2000_0002_0006": "Failed to extrude the filament and the extruder may be clogged.",
|
||||
"12FF_2000_0002_0007": "Failed to check the filament location in the tool head; please click for more help.",
|
||||
"12FF_2000_0003_0007": "Checking the filament location of all AMS slots, please wait.",
|
||||
"12FF_8000_0002_0001": "The filament on the spool holder may be tangled or stuck.",
|
||||
}
|
276
octoprint_bambu_printer/printer/pybambu/const_print_errors.py
Normal file
276
octoprint_bambu_printer/printer/pybambu/const_print_errors.py
Normal file
@ -0,0 +1,276 @@
|
||||
PRINT_ERROR_ERRORS = {
|
||||
"0300_4000": "Printing stopped because homing Z axis failed.",
|
||||
"0300_4001": "The printer timed out waiting for the nozzle to cool down before homing.",
|
||||
"0300_4002": "Printing Stopped because Auto Bed Leveling failed.",
|
||||
"0300_4003": "Nozzle temperature malfunction.",
|
||||
"0300_4004": "Heatbed temperature malfunction.",
|
||||
"0300_4005": "The hotend cooling fan speed is abnormal.",
|
||||
"0300_4006": "The nozzle is clogged.",
|
||||
"0300_4008": "The AMS failed to change filament.",
|
||||
"0300_4009": "Homing XY axis failed.",
|
||||
"0300_400A": "Mechanical resonance frequency identification failed.",
|
||||
"0300_400B": "Internal communication exception.",
|
||||
"0300_400C": "Printing was cancelled.",
|
||||
"0300_400D": "Resume failed after power loss.",
|
||||
"0300_400E": "The motor self-check failed.",
|
||||
"0300_400F": "No build plate is placed.",
|
||||
"0300_8000": "Printing was paused for unknown reason. You can tap 'Resume' to resume the print job.",
|
||||
"0300_8001": "Printing paused. It was due to the pause command added in the printing file.",
|
||||
"0300_8002": "First layer defects were detected by the Micro Lidar. Please check the quality of the printed model before continuing your print.",
|
||||
"0300_8003": "Spaghetti defects were detected by the AI Print Monitoring. Please check the quality of the printed model before continuing your print.",
|
||||
"0300_8004": "Filament ran out. Please load new filament.",
|
||||
"0300_8005": "Toolhead front cover fell off. Please remount the front cover and check to make sure your print is going okay.",
|
||||
"0300_8006": "The build plate marker was not detected. Please confirm the build plate is correctly positioned on the heatbed with all four corners aligned, and the maker is clear.",
|
||||
"0300_8007": "There was an unfinished print job when the printer lost power. If the model is still adhered to the build plate, you can try resuming the print job.",
|
||||
"0300_8008": "Printing Stopped because nozzle temperature problem.",
|
||||
"0300_8009": "Heatbed temperature malfunction.",
|
||||
"0300_800A": "A Filament pile-up was detected by the AI Print Monitoring. Please clean the filament from the waste chute.",
|
||||
"0300_800B": "The cutter is stuck. Please make sure the cutter handle is out, and check the filament sensor cable connection.",
|
||||
"0300_800C": "Skipping step detected, auto-recover complete; please resume print and check if there are any layer shift problems.",
|
||||
"0300_800D": "Some objects have fallen down, or the extruder is not extruding normally. If the defects are acceptable, click 'Resume' button to resume the print job.",
|
||||
"0300_800E": "The print file is not available. Please check to see if the storage media has been removed.",
|
||||
"0300_800F": "The door seems to be open, so printing was paused.",
|
||||
"0300_8010": "The hotend cooling fan speed is abnormal.",
|
||||
"0300_8011": "Detected build plate is not the same as the Gcode file. Please adjust slicer settings or use the correct plate.",
|
||||
"0300_8012": "",
|
||||
"0300_8013": "Printing was paused by the user. You can select 'Resume' to continue printing.",
|
||||
"0300_8014": "The nozzle is covered with filaments, or the build plate is installed incorrectly. Please cancel this printing and clean the nozzle or adjust the build plate according to the actual status, or tap 'Resume' button to resume the print job.",
|
||||
"0300_8015": "The filament on external spool has run out, please load new filament. If the filament is loaded, please select 'Resume'.",
|
||||
"0300_8016": "The nozzle is clogged up with filaments. Please cancel this printing and clean the nozzle according to the actual status, or tap 'Resume' button to resume the print job.",
|
||||
"0300_8017": "Foreign objects detected on heatbed, Please check and clean the heatbed, Then tap 'Resume' button to resume the print job.",
|
||||
"0300_8018": "Chamber temperature malfunction.",
|
||||
"0300_8019": "No build plate is placed.",
|
||||
"0300_801B": "Nozzle temperature problem detected. Refer to the Assistant to re-connect the hotend connector. POWER OFF the printer before this operation to avoid a short circuit.",
|
||||
"0500_4001": "Failed to connect to Bambu Cloud. Please check your network connection.",
|
||||
"0500_4002": "Unsupported print file path or name. Please resend the printing job.",
|
||||
"0500_4003": "Printing stopped because the printer was unable to parse the file. Please resend your print job.",
|
||||
"0500_4004": "The printer can't receive new print jobs while printing. Resend after the current print finishes.",
|
||||
"0500_4005": "Print jobs are not allowed to be sent while updating firmware.",
|
||||
"0500_4006": "There is not enough free storage space for the print job. Restoring to factory settings can release available space.",
|
||||
"0500_4007": "Print jobs are not allowed to be sent while force updating or when repair updating is required.",
|
||||
"0500_4008": "Starting printing failed. please power cycle the printer and resend the print job.",
|
||||
"0500_4009": "Print jobs are not allowed to be sent while updating logs.",
|
||||
"0500_400A": "The file name is not supported. Please rename and restart the printing job.",
|
||||
"0500_400B": "There was a problem downloading a file. Please check you network connection and resend the printing job.",
|
||||
"0500_400C": "Please insert a MicroSD card and restart the printing job.",
|
||||
"0500_400D": "Please run a self-test and restart the printing job.",
|
||||
"0500_400E": "Printing was cancelled.",
|
||||
"0500_4012": "The door seems to be open, so printing was paused.",
|
||||
"0500_4014": "Slicing for the print job failed. Please check your settings and restart the print job.",
|
||||
"0500_4015": "There is not enough free storage space for the print job. Please format or clean MicroSD card to release available space.",
|
||||
"0500_4016": "The MicroSD Card is write-protected. Please replace the MicroSD Card.",
|
||||
"0500_4017": "Binding failed. Please retry or restart the printer and retry.",
|
||||
"0500_4018": "Binding configuration information parsing failed, please try again.",
|
||||
"0500_4019": "The printer has already been bound. Please unbind it and try again.",
|
||||
"0500_401A": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the printer closer to the router or checking the router configuration and then try again.",
|
||||
"0500_401B": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0500_401C": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0500_401D": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
|
||||
"0500_401E": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0500_401F": "Authorization timed out. Please make sure that your phone or PC has access to the internet, and ensure that the Bambu Studio/Bambu Handy APP is running in the foreground during the binding operation.",
|
||||
"0500_4020": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0500_4021": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
|
||||
"0500_4022": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0500_4023": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0500_4024": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the printer closer to the router or checking the router configuration before you try again.",
|
||||
"0500_4025": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0500_4026": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0500_4027": "Cloud access failed; this may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
|
||||
"0500_4028": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0500_4029": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0500_402A": "Failed to connect to the router, which may be caused by wireless interference or being too far away from the router. Please try again or move the printer closer to the router and try again.",
|
||||
"0500_402B": "Router connection failed due to incorrect password. Please check the password and try again.",
|
||||
"0500_402C": "Failed to obtain IP address, which may be caused by wireless interference resulting in data transmission failure or DHCP address pool of the router being full. Please move the printer closer to the router and try again. If the issue persists, please check router settings to see whether the IP addresses have been exhausted.",
|
||||
"0500_402D": "System exception.",
|
||||
"0500_402E": "The system does not support the file system currently used by the Micro SD card. Please replace the Micro SD card or format the current Micro SD card to FAT32.",
|
||||
"0500_402F": "The Micro SD card sector data is damaged. Please use the SD card repair tool to repair or format it. If it still cannot be identified, please replace the Micro SD card.",
|
||||
"0500_4037": "Your sliced file is not compatible with current printer model. This file can't be printed on this printer.",
|
||||
"0500_4038": "The nozzle diameter in sliced file is not consistent with the current nozzle setting. This file can't be printed.",
|
||||
"0500_403A": "The current temperature is too low. In order to protect you and your printer. Printing task, moving axis and other operations are disabled. Please move the printer to an environment above 10 celsius degree.",
|
||||
"0500_8013": "The print file is not available. Please check to see if the storage media has been removed.",
|
||||
"0500_8030": "",
|
||||
"0500_8036": "Your sliced file is not consistent with the current printer model. Continue?",
|
||||
"0500_C010": "MicroSD Card read/write exception. please reinsert or replace MicroSD Card .",
|
||||
"0500_C011": "",
|
||||
"0501_4017": "Binding failed. Please retry or restart the printer and retry.",
|
||||
"0501_4018": "Binding configuration information parsing failed, please try again.",
|
||||
"0501_4019": "The printer has already been bound. Please unbind it and try again.",
|
||||
"0501_401A": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the printer closer to the router or checking the router configuration and then try again.",
|
||||
"0501_401B": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0501_401C": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0501_401D": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
|
||||
"0501_401E": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0501_401F": "Authorization timed out. Please make sure that your phone or PC has access to the internet, and ensure that the Bambu Studio/Bambu Handy APP is running in the foreground during the binding operation.",
|
||||
"0501_4020": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0501_4021": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
|
||||
"0501_4022": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0501_4023": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0501_4024": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the printer closer to the router or checking the router configuration before you try again.",
|
||||
"0501_4025": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0501_4026": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0501_4027": "Cloud access failed; this may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
|
||||
"0501_4028": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0501_4029": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer service.",
|
||||
"0501_4031": "Device discovery binding is in progress, and the QR code cannot be displayed on the screen. You can wait for the binding to finish or abort the device discovery binding process in the APP/Studio and retry scanning the QR code on the screen for binding.",
|
||||
"0501_4032": "QR code binding is in progress, so device discovery binding cannot be performed. You can scan the QR code on the screen for binding or exit the QR code display page on screen and try device discovery binding.",
|
||||
"0501_4033": "Your APP region is not matched with your printer; please download the APP in the corresponding region and register your account again.",
|
||||
"0501_4034": "The slicing progress has not been updated for a long time, and the printing task has exited. Please confirm the parameters and reinitiate printing.",
|
||||
"0501_4035": "The device is in the process of binding and cannot respond to new binding requests.",
|
||||
"0501_4038": "The region settings do not match the printer; please check the printer's region settings.",
|
||||
"0501_4039": "Device login has expired, please try to bind again.",
|
||||
"0502_4001": "Current filament will be used in this print job. Settings cannot be changed.",
|
||||
"0514_039": "Device login has expired; please try to bind again.",
|
||||
"0700_4001": "The AMS has been disabled for a print, but it still has filament loaded. Please unload the AMS filament , and switch to the spool holder filament for printing.",
|
||||
"0700_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.",
|
||||
"0700_8002": "The cutter is stuck. Please make sure the cutter handle is out.",
|
||||
"0700_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
|
||||
"0700_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
|
||||
"0700_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
|
||||
"0700_8006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
|
||||
"0700_8007": "Extruding filament failed. The extruder might be clogged.",
|
||||
"0700_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
|
||||
"0700_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
|
||||
"0700_8012": "Failed to get AMS mapping table; please click 'Retry' to continue.",
|
||||
"0700_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged. After troubleshooting, click the 'Retry' button.",
|
||||
"0701_4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.",
|
||||
"0701_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.",
|
||||
"0701_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.",
|
||||
"0701_8003": "Failed to pull out the filament from the extruder. There may be an extruder clog or broken filament inside the extruder.",
|
||||
"0701_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
|
||||
"0701_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
|
||||
"0701_8006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool.",
|
||||
"0701_8007": "Failed to extrude the filament. Please check if the extruder clogged. After troubleshooting, click the 'Retry' button.",
|
||||
"0701_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
|
||||
"0701_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
|
||||
"0701_8012": "Failed to get AMS mapping table; please click 'Retry' to continue.",
|
||||
"0701_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged. After troubleshooting, click the 'Retry' button.",
|
||||
"0702_4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.",
|
||||
"0702_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.",
|
||||
"0702_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.",
|
||||
"0702_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder. After troubleshooting, click the 'Retry' button.",
|
||||
"0702_8004": "Failed to pull back the filament from the toolhead to AMS. Please check whether the filament or the spool is stuck. After troubleshooting, click the 'Retry' button.",
|
||||
"0702_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
|
||||
"0702_8006": "Failed to feed the filament into the toolhead. Please check whether the filament or the spool is stuck. After troubleshooting, click the 'Retry' button.",
|
||||
"0702_8007": "Failed to extrude the filament. Please check if the extruder clogged. After troubleshooting, click the 'Retry' button.",
|
||||
"0702_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
|
||||
"0702_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
|
||||
"0702_8012": "Failed to get AMS mapping table; please click 'Retry' to continue.",
|
||||
"0702_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged. After troubleshooting, click the 'Retry' button.",
|
||||
"0703_4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.",
|
||||
"0703_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.",
|
||||
"0703_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.",
|
||||
"0703_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder. After troubleshooting, click the 'Retry' button.",
|
||||
"0703_8004": "Failed to pull back the filament from the toolhead to AMS. Please check whether the filament or the spool is stuck. After troubleshooting, click the 'Retry' button.",
|
||||
"0703_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
|
||||
"0703_8006": "Failed to feed the filament into the toolhead. Please check whether the filament or the spool is stuck. After troubleshooting, click the 'Retry' button.",
|
||||
"0703_8007": "Failed to extrude the filament. Please check if the extruder clogged. After troubleshooting, click the 'Retry' button.",
|
||||
"0703_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
|
||||
"0703_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
|
||||
"0703_8012": "Failed to get AMS mapping table; please click 'Retry' to continue.",
|
||||
"0703_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged. After troubleshooting, click the 'Retry' button.",
|
||||
"07FF_4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.",
|
||||
"07FF_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.",
|
||||
"07FF_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.",
|
||||
"07FF_8003": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder. (Connect PTFE tube if you are about to use AMS)",
|
||||
"07FF_8004": "Failed to pull back the filament from the toolhead to AMS. Please check whether the filament or the spool is stuck. After troubleshooting, click the 'retry' button.",
|
||||
"07FF_8005": "Failed to feed the filament outside the AMS. Please clip the end of the filament flat and check to see if the spool is stuck. After troubleshooting, click the 'Retry' button.",
|
||||
"07FF_8006": "Please feed filament into the PTFE tube until it can not be pushed any farther.",
|
||||
"07FF_8007": "Please observe the nozzle. If the filament has been extruded, click 'Done'; if it is not, please push the filament forward slightly and then click 'Retry'.",
|
||||
"07FF_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
|
||||
"07FF_8011": "External filament has run out, please load a new filament.",
|
||||
"07FF_8012": "Failed to get AMS mapping table; please click 'Retry' to continue.",
|
||||
"07FF_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged. After troubleshooting, click the 'Retry' button.",
|
||||
"07FF_C003": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder or PTFE Tube. (Connect PTFE tube if you are about to use AMS)",
|
||||
"07FF_C006": "Please feed filament into the PTFE tube until it can not be pushed any farther.",
|
||||
"07FF_C008": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder. (Connect PTFE tube if you are about to use AMS)",
|
||||
"07FF_C009": "Please feed filament into the PTFE tube until it can not be pushed any farther.",
|
||||
"07FF_C00A": "Please observe the nozzle. If the filament has been extruded, click 'Done'; if it is not, please push the filament forward slightly and then click 'Retry'.",
|
||||
"0C00_402A": "",
|
||||
"0C00_8001": "First layer defects were detected. If the defects are acceptable, click 'Resume' button to resume the print job.",
|
||||
"0C00_8002": "Spaghetti failure was detected.",
|
||||
"0C00_8005": "Purged filament has piled up in the waste chute, which may cause a tool head collision.",
|
||||
"0C00_8009": "Build plate localization marker was not found.",
|
||||
"0C00_800A": "The detected build plate is not the same as in G-code.",
|
||||
"0C00_C003": "Possible defects were detected in the first layer.",
|
||||
"0C00_C004": "Possible spaghetti failure was detected.",
|
||||
"0C00_C006": "Purged filament may have piled up in the waste chute.",
|
||||
"1000_C001": "High bed temperature may lead to filament clogging in the nozzle. You may open the chamber door.",
|
||||
"1000_C002": "Printing CF material with stainless steel may cause nozzle damage.",
|
||||
"1000_C003": "Enabling traditional timelapse might lead to defects. Please enable it as needed?",
|
||||
"1001_C001": "Timelapse is not supported because Spiral vase is enabled in slicing presets.",
|
||||
"1001_C002": "Timelapse is not supported because Print sequence is set to 'By object'.",
|
||||
"1200_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
|
||||
"1200_8001": "Cutting the filament failed. Please check to see if the cutter is stuck. Refer to the Assistant for solutions.",
|
||||
"1200_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.",
|
||||
"1200_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder. After troubleshooting, click the 'Retry' button.",
|
||||
"1200_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck. After troubleshooting, click the 'Retry' button.",
|
||||
"1200_8005": "Failed to feed the filament. Please load the filament, then click the 'Retry' button.",
|
||||
"1200_8006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
|
||||
"1200_8007": "Failed to extrude the filament. This might be caused by clogged extruder or stuck filament. Refer to the Assistant for solutions.",
|
||||
"1200_8010": "Filament or spool may be stuck.",
|
||||
"1200_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
|
||||
"1200_8012": "Failed to get AMS mapping table. Please click the 'Retry' button to continue.",
|
||||
"1200_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged. After troubleshooting, click the 'Retry' button.",
|
||||
"1200_8014": "The filament location in the toolhead was not found. Refer to the Assistant for solutions.",
|
||||
"1200_8015": "Failed to pull out the filament from the toolhead. Please check if the filament is stuck, or the filament is broken inside the extruder or PTFE tube.",
|
||||
"1200_8016": "The extruder is not extruding normally. Refer to the Assistant for troubleshooting. There may be defects in this layer, you may resume if the defects are acceptable.",
|
||||
"1201_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
|
||||
"1201_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.",
|
||||
"1201_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.",
|
||||
"1201_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder. After troubleshooting, click the 'Retry' button.",
|
||||
"1201_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck. After troubleshooting, click the 'Retry' button.",
|
||||
"1201_8005": "Failed to feed the filament. Please load the filament and then click the 'Retry' button.",
|
||||
"1201_8006": "Failed to feed the filament into the toolhead. Please check whether the filament is stuck. After troubleshooting, click the 'Retry' button.",
|
||||
"1201_8007": "Failed to extrude the filament. The extruder may be clogged or the filament may be stuck; please refer to HMS. After troubleshooting, click 'Retry' button.",
|
||||
"1201_8010": "Please check if the spool or filament is stuck. After troubleshooting, click the 'Retry' button.",
|
||||
"1201_8011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
|
||||
"1201_8012": "Failed to get AMS mapping table; please click the 'Retry' button to continue.",
|
||||
"1201_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged. After troubleshooting, click the 'Retry' button.",
|
||||
"1201_8014": "Failed to check the filament location in the tool head; please refer to the HMS. After troubleshooting, click the 'Retry' button.",
|
||||
"1201_8015": "Failed to pull back the filament from the toolhead. Please check if the filament is stuck or the filament is broken inside the extruder. After troubleshooting, click the 'Retry' button.",
|
||||
"1201_8016": "The extruder is not extruding normally; please refer to the HMS. After trouble shooting. If the defects are acceptable, please click 'Retry' button.",
|
||||
"1202_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
|
||||
"1202_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.",
|
||||
"1202_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.",
|
||||
"1202_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder. After troubleshooting, click the 'Retry' button.",
|
||||
"1202_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck. After troubleshooting, click the 'Retry' button.",
|
||||
"1202_8005": "Failed to feed the filament. Please load the filament, then click the 'Retry' button.",
|
||||
"1202_8006": "Failed to feed the filament into the toolhead. Please check whether the filament is stuck. After troubleshooting, click the 'Retry' button.",
|
||||
"1202_8007": "Failed to extrude the filament. The extruder may be clogged or the filament may be stuck; please refer to HMS. After troubleshooting, click 'Retry' button.",
|
||||
"1202_8010": "Please check if the spool or filament is stuck. After troubleshooting, click the 'Retry' button.",
|
||||
"1202_8011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
|
||||
"1202_8012": "Failed to get AMS mapping table; please click the 'Retry' button to continue.",
|
||||
"1202_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged. After troubleshooting, click the 'Retry' button.",
|
||||
"1202_8014": "Failed to check the filament location in the tool head; please refer to the HMS. After troubleshooting, click the 'Retry' button.",
|
||||
"1202_8015": "Failed to pull back the filament from the toolhead. Please check if the filament is stuck or is broken inside the extruder. After troubleshooting, click the 'Retry' button.",
|
||||
"1202_8016": "The extruder is not extruding normally; please refer to the HMS. After trouble shooting. If the defects are acceptable, please click 'Retry' button.",
|
||||
"1203_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
|
||||
"1203_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.",
|
||||
"1203_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.",
|
||||
"1203_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder. After troubleshooting, click the 'Retry' button.",
|
||||
"1203_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck. After troubleshooting, click the 'Retry' button.",
|
||||
"1203_8005": "Failed to feed the filament. Please load the filament, then click the 'Retry' button.",
|
||||
"1203_8006": "Failed to feed the filament into the toolhead. Please check whether the filament is stuck. After troubleshooting, click the 'Retry' button.",
|
||||
"1203_8007": "Failed to extrude the filament. The extruder may be clogged or the filament may be stuck; please refer to HMS. After troubleshooting, click 'Retry' button.",
|
||||
"1203_8010": "Please check if the spool or filament is stuck. After troubleshooting, click the 'Retry' button.",
|
||||
"1203_8011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
|
||||
"1203_8012": "Failed to get AMS mapping table; please click the 'Retry' button to continue.",
|
||||
"1203_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged. After troubleshooting, click the 'Retry' button.",
|
||||
"1203_8014": "Failed to check the filament location in the tool head; please refer to the HMS. After troubleshooting, click the 'Retry' button.",
|
||||
"1203_8015": "Failed to pull back the filament from the toolhead. Please check if the filament is stuck or is broken inside the extruder. After troubleshooting, click the 'Retry' button.",
|
||||
"1203_8016": "The extruder is not extruding normally; please refer to the HMS. After trouble shooting. If the defects are acceptable, please click 'Retry' button.",
|
||||
"12FF_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
|
||||
"12FF_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.",
|
||||
"12FF_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.",
|
||||
"12FF_8003": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder or PTFE Tube. (Connect PTFE tube if you are about to use AMS)",
|
||||
"12FF_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck. After troubleshooting, click the 'Retry' button.",
|
||||
"12FF_8005": "Failed to feed the filament. Please load the filament, then click the 'Retry' button.",
|
||||
"12FF_8006": "Please feed filament into the PTFE tube until it can not be pushed any farther.",
|
||||
"12FF_8007": "Check nozzle. Click 'Done' if filament was extruded, otherwise push filament forward slightly and click 'Retry.'",
|
||||
"12FF_8010": "Please check if the filament or the spool is stuck.",
|
||||
"12FF_8011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
|
||||
"12FF_8012": "Failed to get AMS mapping table; please click the 'Retry' button to continue.",
|
||||
"12FF_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged. After troubleshooting, click the 'Retry' button.",
|
||||
"12FF_C003": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder or PTFE Tube. (Connect PTFE tube if you are about to use AMS)",
|
||||
"12FF_C006": "Please feed filament into the PTFE tube until it can not be pushed any farther.",
|
||||
}
|
85
octoprint_bambu_printer/printer/pybambu/filaments.json
Normal file
85
octoprint_bambu_printer/printer/pybambu/filaments.json
Normal file
@ -0,0 +1,85 @@
|
||||
{
|
||||
"GFA00": "Bambu PLA Basic",
|
||||
"GFA01": "Bambu PLA Matte",
|
||||
"GFA02": "Bambu PLA Metal",
|
||||
"GFA05": "Bambu PLA Silk",
|
||||
"GFA07": "Bambu PLA Marble",
|
||||
"GFA08": "Bambu PLA Sparkle",
|
||||
"GFA09": "Bambu PLA Tough",
|
||||
"GFA11": "Bambu PLA Aero",
|
||||
"GFA12": "Bambu PLA Glow",
|
||||
"GFA13": "Bambu PLA Dynamic",
|
||||
"GFA15": "Bambu PLA Galaxy",
|
||||
"GFA50": "Bambu PLA-CF",
|
||||
"GFB00": "Bambu ABS",
|
||||
"GFB01": "Bambu ASA",
|
||||
"GFB02": "Bambu ASA-Aero",
|
||||
"GFB50": "Bambu ABS-GF",
|
||||
"GFB51": "Bambu ASA-CF",
|
||||
"GFB60": "PolyLite ABS",
|
||||
"GFB61": "PolyLite ASA",
|
||||
"GFB98": "Generic ASA",
|
||||
"GFB99": "Generic ABS",
|
||||
"GFC00": "Bambu PC",
|
||||
"GFC99": "Generic PC",
|
||||
"GFG00": "Bambu PETG Basic",
|
||||
"GFG01": "Bambu PETG Translucent",
|
||||
"GFG02": "Bambu PETG HF",
|
||||
"GFG50": "Bambu PETG-CF",
|
||||
"GFG60": "PolyLite PETG",
|
||||
"GFG96": "Generic PETG HF",
|
||||
"GFG97": "Generic PCTG",
|
||||
"GFG98": "Generic PETG-CF",
|
||||
"GFG99": "Generic PETG",
|
||||
"GFL00": "PolyLite PLA",
|
||||
"GFL01": "PolyTerra PLA",
|
||||
"GFL03": "eSUN PLA+",
|
||||
"GFL04": "Overture PLA",
|
||||
"GFL05": "Overture Matte PLA",
|
||||
"GFL06": "Fiberon PETG-ESD",
|
||||
"GFL50": "Fiberon PA6-CF",
|
||||
"GFL51": "Fiberon PA6-GF",
|
||||
"GFL52": "Fiberon PA12-CF",
|
||||
"GFL53": "Fiberon PA612-CF",
|
||||
"GFL54": "Fiberon PET-CF",
|
||||
"GFL55": "Fiberon PETG-rCF",
|
||||
"GFL95": "Generic PLA High Speed",
|
||||
"GFL96": "Generic PLA Silk",
|
||||
"GFL98": "Generic PLA-CF",
|
||||
"GFL99": "Generic PLA",
|
||||
"GFN03": "Bambu PA-CF",
|
||||
"GFN04": "Bambu PAHT-CF",
|
||||
"GFN05": "Bambu PA6-CF",
|
||||
"GFN06": "Bambu PPA-CF",
|
||||
"GFN08": "Bambu PA6-GF",
|
||||
"GFN96": "Generic PPA-GF",
|
||||
"GFN97": "Generic PPA-CF",
|
||||
"GFN98": "Generic PA-CF",
|
||||
"GFN99": "Generic PA",
|
||||
"GFP95": "Generic PP-GF",
|
||||
"GFP96": "Generic PP-CF",
|
||||
"GFP97": "Generic PP",
|
||||
"GFP98": "Generic PE-CF",
|
||||
"GFP99": "Generic PE",
|
||||
"GFR98": "Generic PHA",
|
||||
"GFR99": "Generic EVA",
|
||||
"GFS00": "Bambu Support W",
|
||||
"GFS01": "Bambu Support G",
|
||||
"GFS02": "Bambu Support For PLA",
|
||||
"GFS03": "Bambu Support For PA/PET",
|
||||
"GFS04": "Bambu PVA",
|
||||
"GFS05": "Bambu Support For PLA/PETG",
|
||||
"GFS06": "Bambu Support for ABS",
|
||||
"GFS97": "Generic BVOH",
|
||||
"GFS98": "Generic HIPS",
|
||||
"GFS99": "Generic PVA",
|
||||
"GFT01": "Bambu PET-CF",
|
||||
"GFT02": "Bambu PPS-CF",
|
||||
"GFT97": "Generic PPS",
|
||||
"GFT98": "Generic PPS-CF",
|
||||
"GFU00": "Bambu TPU 95A HF",
|
||||
"GFU01": "Bambu TPU 95A",
|
||||
"GFU02": "Bambu TPU for AMS",
|
||||
"GFU98": "Generic TPU for AMS",
|
||||
"GFU99": "Generic TPU"
|
||||
}
|
1483
octoprint_bambu_printer/printer/pybambu/models.py
Normal file
1483
octoprint_bambu_printer/printer/pybambu/models.py
Normal file
File diff suppressed because it is too large
Load Diff
248
octoprint_bambu_printer/printer/pybambu/utils.py
Normal file
248
octoprint_bambu_printer/printer/pybambu/utils.py
Normal file
@ -0,0 +1,248 @@
|
||||
import math
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from .const import (
|
||||
CURRENT_STAGE_IDS,
|
||||
SPEED_PROFILE,
|
||||
FILAMENT_NAMES,
|
||||
HMS_ERRORS,
|
||||
HMS_AMS_ERRORS,
|
||||
PRINT_ERROR_ERRORS,
|
||||
HMS_SEVERITY_LEVELS,
|
||||
HMS_MODULES,
|
||||
LOGGER,
|
||||
BAMBU_URL,
|
||||
FansEnum,
|
||||
TempEnum
|
||||
)
|
||||
from .commands import SEND_GCODE_TEMPLATE
|
||||
|
||||
|
||||
def search(lst, predicate, default={}):
|
||||
"""Search an array for a string"""
|
||||
for item in lst:
|
||||
if predicate(item):
|
||||
return item
|
||||
return default
|
||||
|
||||
|
||||
def fan_percentage(speed):
|
||||
"""Converts a fan speed to percentage"""
|
||||
if not speed:
|
||||
return 0
|
||||
percentage = (int(speed) / 15) * 100
|
||||
return round(percentage / 10) * 10
|
||||
|
||||
|
||||
def fan_percentage_to_gcode(fan: FansEnum, percentage: int):
|
||||
"""Converts a fan speed percentage to the gcode command to set that"""
|
||||
if fan == FansEnum.PART_COOLING:
|
||||
fanString = "P1"
|
||||
elif fan == FansEnum.AUXILIARY:
|
||||
fanString = "P2"
|
||||
elif fan == FansEnum.CHAMBER:
|
||||
fanString = "P3"
|
||||
|
||||
percentage = round(percentage / 10) * 10
|
||||
speed = math.ceil(255 * percentage / 100)
|
||||
command = SEND_GCODE_TEMPLATE
|
||||
command['print']['param'] = f"M106 {fanString} S{speed}\n"
|
||||
return command
|
||||
|
||||
|
||||
def set_temperature_to_gcode(temp: TempEnum, temperature: int):
|
||||
"""Converts a temperature to the gcode command to set that"""
|
||||
if temp == TempEnum.NOZZLE:
|
||||
tempCommand = "M104"
|
||||
elif temp == TempEnum.HEATBED:
|
||||
tempCommand = "M140"
|
||||
|
||||
command = SEND_GCODE_TEMPLATE
|
||||
command['print']['param'] = f"{tempCommand} S{temperature}\n"
|
||||
return command
|
||||
|
||||
|
||||
def to_whole(number):
|
||||
if not number:
|
||||
return 0
|
||||
return round(number)
|
||||
|
||||
|
||||
def get_filament_name(idx, custom_filaments: dict):
|
||||
"""Converts a filament idx to a human-readable name"""
|
||||
result = FILAMENT_NAMES.get(idx, "unknown")
|
||||
if result == "unknown" and idx != "":
|
||||
result = custom_filaments.get(idx, "unknown")
|
||||
# if result == "unknown" and idx != "":
|
||||
# LOGGER.debug(f"UNKNOWN FILAMENT IDX: '{idx}'")
|
||||
return result
|
||||
|
||||
|
||||
def get_speed_name(id):
|
||||
"""Return the human-readable name for a speed id"""
|
||||
return SPEED_PROFILE.get(int(id), "standard")
|
||||
|
||||
|
||||
def get_current_stage(id) -> str:
|
||||
"""Return the human-readable description for a stage action"""
|
||||
return CURRENT_STAGE_IDS.get(int(id), "unknown")
|
||||
|
||||
|
||||
def get_HMS_error_text(hms_code: str):
|
||||
"""Return the human-readable description for an HMS error"""
|
||||
|
||||
ams_code = get_generic_AMS_HMS_error_code(hms_code)
|
||||
ams_error = HMS_AMS_ERRORS.get(ams_code, "")
|
||||
if ams_error != "":
|
||||
# 070X_xYxx_xxxx_xxxx = AMS X (0 based index) Slot Y (0 based index) has the error
|
||||
ams_index = int(hms_code[3:4], 16) + 1
|
||||
ams_slot = int(hms_code[6:7], 16) + 1
|
||||
ams_error = ams_error.replace('AMS1', f"AMS{ams_index}")
|
||||
ams_error = ams_error.replace('slot 1', f"slot {ams_slot}")
|
||||
return ams_error
|
||||
|
||||
return HMS_ERRORS.get(hms_code, "unknown")
|
||||
|
||||
|
||||
def get_print_error_text(print_error_code: str):
|
||||
"""Return the human-readable description for a print error"""
|
||||
|
||||
hex_conversion = f'0{int(print_error_code):x}'
|
||||
print_error_code = hex_conversion[slice(0,4,1)] + "_" + hex_conversion[slice(4,8,1)]
|
||||
print_error = PRINT_ERROR_ERRORS.get(print_error_code.upper(), "")
|
||||
if print_error != "":
|
||||
return print_error
|
||||
|
||||
return PRINT_ERROR_ERRORS.get(print_error_code, "unknown")
|
||||
|
||||
|
||||
def get_HMS_severity(code: int) -> str:
|
||||
uint_code = code >> 16
|
||||
if code > 0 and uint_code in HMS_SEVERITY_LEVELS:
|
||||
return HMS_SEVERITY_LEVELS[uint_code]
|
||||
return HMS_SEVERITY_LEVELS["default"]
|
||||
|
||||
|
||||
def get_HMS_module(attr: int) -> str:
|
||||
uint_attr = (attr >> 24) & 0xFF
|
||||
if attr > 0 and uint_attr in HMS_MODULES:
|
||||
return HMS_MODULES[uint_attr]
|
||||
return HMS_MODULES["default"]
|
||||
|
||||
|
||||
def get_generic_AMS_HMS_error_code(hms_code: str):
|
||||
code1 = int(hms_code[0:4], 16)
|
||||
code2 = int(hms_code[5:9], 16)
|
||||
code3 = int(hms_code[10:14], 16)
|
||||
code4 = int(hms_code[15:19], 16)
|
||||
|
||||
# 070X_xYxx_xxxx_xxxx = AMS X (0 based index) Slot Y (0 based index) has the error
|
||||
ams_code = f"{code1 & 0xFFF8:0>4X}_{code2 & 0xF8FF:0>4X}_{code3:0>4X}_{code4:0>4X}"
|
||||
ams_error = HMS_AMS_ERRORS.get(ams_code, "")
|
||||
if ams_error != "":
|
||||
return ams_code
|
||||
|
||||
return f"{code1:0>4X}_{code2:0>4X}_{code3:0>4X}_{code4:0>4X}"
|
||||
|
||||
|
||||
def get_printer_type(modules, default):
|
||||
# Known possible values:
|
||||
#
|
||||
# A1/P1 printers are of the form:
|
||||
# {
|
||||
# "name": "esp32",
|
||||
# "project_name": "C11",
|
||||
# "sw_ver": "01.07.23.47",
|
||||
# "hw_ver": "AP04",
|
||||
# "sn": "**REDACTED**",
|
||||
# "flag": 0
|
||||
# },
|
||||
# P1P = AP04 / C11
|
||||
# P1S = AP04 / C12
|
||||
# A1Mini = AP05 / N1 or AP04 / N1 or AP07 / N1
|
||||
# A1 = AP05 / N2S
|
||||
#
|
||||
# X1C printers are of the form:
|
||||
# {
|
||||
# "hw_ver": "AP05",
|
||||
# "name": "rv1126",
|
||||
# "sn": "**REDACTED**",
|
||||
# "sw_ver": "00.00.28.55"
|
||||
# },
|
||||
# X1C = AP05
|
||||
#
|
||||
# X1E printers are of the form:
|
||||
# {
|
||||
# "flag": 0,
|
||||
# "hw_ver": "AP02",
|
||||
# "name": "ap",
|
||||
# "sn": "**REDACTED**",
|
||||
# "sw_ver": "00.00.32.14"
|
||||
# }
|
||||
# X1E = AP02
|
||||
|
||||
apNode = search(modules, lambda x: x.get('hw_ver', "").find("AP0") == 0)
|
||||
if len(apNode.keys()) > 1:
|
||||
hw_ver = apNode['hw_ver']
|
||||
project_name = apNode.get('project_name', '')
|
||||
if hw_ver == 'AP02':
|
||||
return 'X1E'
|
||||
elif project_name == 'N1':
|
||||
return 'A1MINI'
|
||||
elif hw_ver == 'AP04':
|
||||
if project_name == 'C11':
|
||||
return 'P1P'
|
||||
if project_name == 'C12':
|
||||
return 'P1S'
|
||||
elif hw_ver == 'AP05':
|
||||
if project_name == 'N2S':
|
||||
return 'A1'
|
||||
if project_name == '':
|
||||
return 'X1C'
|
||||
LOGGER.debug(f"UNKNOWN DEVICE: hw_ver='{hw_ver}' / project_name='{project_name}'")
|
||||
return default
|
||||
|
||||
|
||||
def get_hw_version(modules, default):
|
||||
"""Retrieve hardware version of printer"""
|
||||
apNode = search(modules, lambda x: x.get('hw_ver', "").find("AP0") == 0)
|
||||
if len(apNode.keys()) > 1:
|
||||
return apNode.get("hw_ver")
|
||||
return default
|
||||
|
||||
|
||||
def get_sw_version(modules, default):
|
||||
"""Retrieve software version of printer"""
|
||||
ota = search(modules, lambda x: x.get('name', "") == "ota")
|
||||
if len(ota.keys()) > 1:
|
||||
return ota.get("sw_ver")
|
||||
return default
|
||||
|
||||
|
||||
def get_start_time(timestamp):
|
||||
"""Return start time of a print"""
|
||||
if timestamp == 0:
|
||||
return None
|
||||
return datetime.fromtimestamp(timestamp)
|
||||
|
||||
|
||||
def get_end_time(remaining_time):
|
||||
"""Calculate the end time of a print"""
|
||||
end_time = round_minute(datetime.now() + timedelta(minutes=remaining_time))
|
||||
return end_time
|
||||
|
||||
|
||||
def round_minute(date: datetime = None, round_to: int = 1):
|
||||
""" Round datetime object to minutes"""
|
||||
if not date:
|
||||
date = datetime.now()
|
||||
date = date.replace(second=0, microsecond=0)
|
||||
delta = date.minute % round_to
|
||||
return date.replace(minute=date.minute - delta)
|
||||
|
||||
|
||||
def get_Url(url: str, region: str):
|
||||
urlstr = BAMBU_URL[url]
|
||||
if region == "China":
|
||||
urlstr = urlstr.replace('.com', '.cn')
|
||||
return urlstr
|
0
octoprint_bambu_printer/printer/states/__init__.py
Normal file
0
octoprint_bambu_printer/printer/states/__init__.py
Normal file
46
octoprint_bambu_printer/printer/states/a_printer_state.py
Normal file
46
octoprint_bambu_printer/printer/states/a_printer_state.py
Normal file
@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from octoprint_bambu_printer.printer.bambu_virtual_printer import (
|
||||
BambuVirtualPrinter,
|
||||
)
|
||||
|
||||
|
||||
class APrinterState:
|
||||
def __init__(self, printer: BambuVirtualPrinter) -> None:
|
||||
self._log = logging.getLogger(
|
||||
"octoprint.plugins.bambu_printer.BambuPrinter.states"
|
||||
)
|
||||
self._printer = printer
|
||||
|
||||
def init(self):
|
||||
pass
|
||||
|
||||
def finalize(self):
|
||||
pass
|
||||
|
||||
def handle_gcode(self, gcode):
|
||||
self._log.debug(f"{self.__class__.__name__} gcode execution disabled")
|
||||
|
||||
def update_print_job_info(self):
|
||||
self._log_skip_state_transition("start_new_print")
|
||||
|
||||
def start_new_print(self):
|
||||
self._log_skip_state_transition("start_new_print")
|
||||
|
||||
def pause_print(self):
|
||||
self._log_skip_state_transition("pause_print")
|
||||
|
||||
def cancel_print(self):
|
||||
self._log_skip_state_transition("cancel_print")
|
||||
|
||||
def resume_print(self):
|
||||
self._log_skip_state_transition("resume_print")
|
||||
|
||||
def _log_skip_state_transition(self, method):
|
||||
self._log.debug(
|
||||
f"skipping {self.__class__.__name__} state transition for '{method}'"
|
||||
)
|
56
octoprint_bambu_printer/printer/states/idle_state.py
Normal file
56
octoprint_bambu_printer/printer/states/idle_state.py
Normal file
@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from octoprint_bambu_printer.printer.file_system.file_info import FileInfo
|
||||
from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState
|
||||
|
||||
|
||||
class IdleState(APrinterState):
|
||||
|
||||
def start_new_print(self):
|
||||
selected_file = self._printer.selected_file
|
||||
if selected_file is None:
|
||||
self._log.warn("Cannot start print job if file was not selected")
|
||||
return
|
||||
|
||||
print_command = self._get_print_command_for_file(selected_file)
|
||||
self._log.debug(f"Sending print command: {print_command}")
|
||||
if self._printer.bambu_client.publish(print_command):
|
||||
self._log.info(f"Started print for {selected_file.file_name}")
|
||||
else:
|
||||
self._log.warn(f"Failed to start print for {selected_file.file_name}")
|
||||
|
||||
def _get_print_command_for_file(self, selected_file: FileInfo):
|
||||
|
||||
# URL to print. Root path, protocol can vary. E.g., if sd card, "ftp:///myfile.3mf", "ftp:///cache/myotherfile.3mf"
|
||||
filesystem_root = (
|
||||
"file:///mnt/sdcard/"
|
||||
if self._printer._settings.get(["device_type"]) in ["X1", "X1C"]
|
||||
else "file:///sdcard/"
|
||||
)
|
||||
|
||||
print_command = {
|
||||
"print": {
|
||||
"sequence_id": 0,
|
||||
"command": "project_file",
|
||||
"param": "Metadata/plate_1.gcode",
|
||||
"md5": "",
|
||||
"profile_id": "0",
|
||||
"project_id": "0",
|
||||
"subtask_id": "0",
|
||||
"task_id": "0",
|
||||
"subtask_name": selected_file.file_name,
|
||||
"url": f"{filesystem_root}{selected_file.path.as_posix()}",
|
||||
"bed_type": "auto",
|
||||
"timelapse": self._printer._settings.get_boolean(["timelapse"]),
|
||||
"bed_leveling": self._printer._settings.get_boolean(["bed_leveling"]),
|
||||
"flow_cali": self._printer._settings.get_boolean(["flow_cali"]),
|
||||
"vibration_cali": self._printer._settings.get_boolean(
|
||||
["vibration_cali"]
|
||||
),
|
||||
"layer_inspect": self._printer._settings.get_boolean(["layer_inspect"]),
|
||||
"use_ams": self._printer._settings.get_boolean(["use_ams"]),
|
||||
"ams_mapping": self._printer._settings.get(["ams_mapping"]),
|
||||
}
|
||||
}
|
||||
|
||||
return print_command
|
51
octoprint_bambu_printer/printer/states/paused_state.py
Normal file
51
octoprint_bambu_printer/printer/states/paused_state.py
Normal file
@ -0,0 +1,51 @@
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from octoprint_bambu_printer.printer.bambu_virtual_printer import (
|
||||
BambuVirtualPrinter,
|
||||
)
|
||||
|
||||
import threading
|
||||
|
||||
import octoprint_bambu_printer.printer.pybambu.commands
|
||||
from octoprint.util import RepeatedTimer
|
||||
|
||||
from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState
|
||||
|
||||
|
||||
class PausedState(APrinterState):
|
||||
|
||||
def __init__(self, printer: BambuVirtualPrinter) -> None:
|
||||
super().__init__(printer)
|
||||
self._pausedLock = threading.Event()
|
||||
self._paused_repeated_report = None
|
||||
|
||||
def init(self):
|
||||
if not self._pausedLock.is_set():
|
||||
self._pausedLock.set()
|
||||
|
||||
self._printer.sendIO("// action:paused")
|
||||
self._printer.start_continuous_status_report(3)
|
||||
|
||||
def finalize(self):
|
||||
if self._pausedLock.is_set():
|
||||
self._pausedLock.clear()
|
||||
if self._paused_repeated_report is not None:
|
||||
self._paused_repeated_report.join()
|
||||
self._paused_repeated_report = None
|
||||
|
||||
def start_new_print(self):
|
||||
if self._printer.bambu_client.connected:
|
||||
if self._printer.bambu_client.publish(octoprint_bambu_printer.printer.pybambu.commands.RESUME):
|
||||
self._log.info("print resumed")
|
||||
else:
|
||||
self._log.info("print resume failed")
|
||||
|
||||
def cancel_print(self):
|
||||
if self._printer.bambu_client.connected:
|
||||
if self._printer.bambu_client.publish(octoprint_bambu_printer.printer.pybambu.commands.STOP):
|
||||
self._log.info("print cancelled")
|
||||
self._printer.finalize_print_job()
|
||||
else:
|
||||
self._log.info("print cancel failed")
|
102
octoprint_bambu_printer/printer/states/printing_state.py
Normal file
102
octoprint_bambu_printer/printer/states/printing_state.py
Normal file
@ -0,0 +1,102 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from octoprint_bambu_printer.printer.bambu_virtual_printer import (
|
||||
BambuVirtualPrinter,
|
||||
)
|
||||
|
||||
import threading
|
||||
|
||||
import octoprint_bambu_printer.printer.pybambu
|
||||
import octoprint_bambu_printer.printer.pybambu.models
|
||||
import octoprint_bambu_printer.printer.pybambu.commands
|
||||
|
||||
from octoprint_bambu_printer.printer.print_job import PrintJob
|
||||
from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState
|
||||
|
||||
|
||||
class PrintingState(APrinterState):
|
||||
|
||||
def __init__(self, printer: BambuVirtualPrinter) -> None:
|
||||
super().__init__(printer)
|
||||
self._printer.current_print_job = None
|
||||
self._is_printing = False
|
||||
self._sd_printing_thread = None
|
||||
|
||||
def init(self):
|
||||
self._is_printing = True
|
||||
self._printer.remove_project_selection()
|
||||
self.update_print_job_info()
|
||||
self._start_worker_thread()
|
||||
|
||||
def finalize(self):
|
||||
if self._sd_printing_thread is not None and self._sd_printing_thread.is_alive():
|
||||
self._is_printing = False
|
||||
self._sd_printing_thread.join()
|
||||
self._sd_printing_thread = None
|
||||
self._printer.current_print_job = None
|
||||
|
||||
def _start_worker_thread(self):
|
||||
self._is_printing = True
|
||||
if self._sd_printing_thread is None:
|
||||
self._sd_printing_thread = threading.Thread(target=self._printing_worker)
|
||||
self._sd_printing_thread.start()
|
||||
else:
|
||||
self._sd_printing_thread.join()
|
||||
|
||||
def _printing_worker(self):
|
||||
self._log.debug(f"_printing_worker before while loop: {self._printer.current_print_job}")
|
||||
while (
|
||||
self._is_printing
|
||||
and self._printer.current_print_job is not None
|
||||
and self._printer.current_print_job.progress < 100
|
||||
):
|
||||
self.update_print_job_info()
|
||||
self._printer.report_print_job_status()
|
||||
time.sleep(3)
|
||||
|
||||
self._log.debug(f"_printing_worker after while loop: {self._printer.current_print_job}")
|
||||
self.update_print_job_info()
|
||||
if (
|
||||
self._printer.current_print_job is not None
|
||||
and self._printer.current_print_job.progress >= 100
|
||||
):
|
||||
self._printer.finalize_print_job()
|
||||
|
||||
def update_print_job_info(self):
|
||||
print_job_info = self._printer.bambu_client.get_device().print_job
|
||||
subtask_name: str = print_job_info.subtask_name
|
||||
gcode_file: str = print_job_info.gcode_file
|
||||
|
||||
self._log.debug(f"update_print_job_info: {print_job_info}")
|
||||
|
||||
project_file_info = self._printer.project_files.get_file_by_name(subtask_name) or self._printer.project_files.get_file_by_name(gcode_file)
|
||||
if project_file_info is None:
|
||||
self._log.debug(f"No 3mf file found for {print_job_info}")
|
||||
self._printer.current_print_job = None
|
||||
self._printer.change_state(self._printer._state_idle)
|
||||
return
|
||||
|
||||
progress = print_job_info.print_percentage
|
||||
if print_job_info.gcode_state == "PREPARE" and progress == 100:
|
||||
progress = 0
|
||||
self._printer.current_print_job = PrintJob(project_file_info, progress, print_job_info.remaining_time, print_job_info.current_layer, print_job_info.total_layers)
|
||||
self._printer.select_project_file(project_file_info.path.as_posix())
|
||||
|
||||
def pause_print(self):
|
||||
if self._printer.bambu_client.connected:
|
||||
if self._printer.bambu_client.publish(octoprint_bambu_printer.printer.pybambu.commands.PAUSE):
|
||||
self._log.info("print paused")
|
||||
else:
|
||||
self._log.info("print pause failed")
|
||||
|
||||
def cancel_print(self):
|
||||
if self._printer.bambu_client.connected:
|
||||
if self._printer.bambu_client.publish(octoprint_bambu_printer.printer.pybambu.commands.STOP):
|
||||
self._log.info("print cancelled")
|
||||
self._printer.finalize_print_job()
|
||||
else:
|
||||
self._log.info("print cancel failed")
|
24
octoprint_bambu_printer/static/css/bambu_printer.css
Normal file
24
octoprint_bambu_printer/static/css/bambu_printer.css
Normal file
@ -0,0 +1,24 @@
|
||||
#sidebar_plugin_bambu_printer div.well {
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
#sidebar_plugin_bambu_printer div.well div.span3.text-center div.row-fluid {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#sidebar_plugin_bambu_printer div.well div.span3.text-center div.row-fluid.active {
|
||||
border: 2px solid;
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#bambu_printer_print_options div.well {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
#bambu_printer_print_options div.modal-body {
|
||||
overflow: inherit !important;
|
||||
}
|
||||
|
||||
|
@ -4,26 +4,187 @@
|
||||
* Author: jneilliii
|
||||
* License: AGPLv3
|
||||
*/
|
||||
$(function() {
|
||||
|
||||
$(function () {
|
||||
function Bambu_printerViewModel(parameters) {
|
||||
var self = this;
|
||||
|
||||
// assign the injected parameters, e.g.:
|
||||
// self.loginStateViewModel = parameters[0];
|
||||
// self.settingsViewModel = parameters[1];
|
||||
self.settingsViewModel = parameters[0];
|
||||
self.filesViewModel = parameters[1];
|
||||
self.loginStateViewModel = parameters[2];
|
||||
self.accessViewModel = parameters[3];
|
||||
self.timelapseViewModel = parameters[4];
|
||||
|
||||
// TODO: Implement your plugin's view model here.
|
||||
self.use_ams = true;
|
||||
self.ams_mapping = ko.observableArray([]);
|
||||
|
||||
self.job_info = ko.observable();
|
||||
|
||||
self.auth_type = ko.observable("");
|
||||
|
||||
self.show_password = ko.pureComputed(function(){
|
||||
return self.settingsViewModel.settings.plugins.bambu_printer.auth_token() === '';
|
||||
});
|
||||
|
||||
self.show_verification = ko.pureComputed(function(){
|
||||
return self.auth_type() !== '';
|
||||
});
|
||||
|
||||
self.ams_mapping_computed = function(){
|
||||
var output_list = [];
|
||||
var index = 0;
|
||||
|
||||
ko.utils.arrayForEach(self.settingsViewModel.settings.plugins.bambu_printer.ams_data(), function(item){
|
||||
if(item){
|
||||
output_list = output_list.concat(item.tray());
|
||||
}
|
||||
});
|
||||
|
||||
ko.utils.arrayForEach(output_list, function(item){
|
||||
item["index"] = ko.observable(index);
|
||||
index++;
|
||||
});
|
||||
|
||||
return output_list;
|
||||
};
|
||||
|
||||
self.getAuthToken = function (data) {
|
||||
self.settingsViewModel.settings.plugins.bambu_printer.auth_token("");
|
||||
self.auth_type("");
|
||||
OctoPrint.simpleApiCommand("bambu_printer", "register", {
|
||||
"email": self.settingsViewModel.settings.plugins.bambu_printer.email(),
|
||||
"password": $("#bambu_cloud_password").val(),
|
||||
"region": self.settingsViewModel.settings.plugins.bambu_printer.region(),
|
||||
"auth_token": self.settingsViewModel.settings.plugins.bambu_printer.auth_token()
|
||||
})
|
||||
.done(function (response) {
|
||||
self.auth_type(response.auth_response);
|
||||
});
|
||||
};
|
||||
|
||||
self.verifyCode = function (data) {
|
||||
self.settingsViewModel.settings.plugins.bambu_printer.auth_token("");
|
||||
OctoPrint.simpleApiCommand("bambu_printer", "verify", {
|
||||
"password": $("#bambu_cloud_verify_code").val(),
|
||||
"auth_type": self.auth_type(),
|
||||
})
|
||||
.done(function (response) {
|
||||
console.log(response);
|
||||
if (response.auth_token) {
|
||||
self.settingsViewModel.settings.plugins.bambu_printer.auth_token(response.auth_token);
|
||||
self.settingsViewModel.settings.plugins.bambu_printer.username(response.username);
|
||||
self.auth_type("");
|
||||
} else if (response.error) {
|
||||
self.settingsViewModel.settings.plugins.bambu_printer.auth_token("");
|
||||
$("#bambu_cloud_verify_code").val("");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// initialize list helper
|
||||
self.listHelper = new ItemListHelper(
|
||||
"timelapseFiles",
|
||||
{
|
||||
name: function (a, b) {
|
||||
// sorts ascending
|
||||
if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase())
|
||||
return -1;
|
||||
if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase())
|
||||
return 1;
|
||||
return 0;
|
||||
},
|
||||
date: function (a, b) {
|
||||
// sorts descending
|
||||
if (a["date"] > b["date"]) return -1;
|
||||
if (a["date"] < b["date"]) return 1;
|
||||
return 0;
|
||||
},
|
||||
size: function (a, b) {
|
||||
// sorts descending
|
||||
if (a["bytes"] > b["bytes"]) return -1;
|
||||
if (a["bytes"] < b["bytes"]) return 1;
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
{},
|
||||
"name",
|
||||
[],
|
||||
[],
|
||||
CONFIG_TIMELAPSEFILESPERPAGE
|
||||
);
|
||||
|
||||
self.onDataUpdaterPluginMessage = function(plugin, data) {
|
||||
if (plugin != "bambu_printer") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.files !== undefined) {
|
||||
self.listHelper.updateItems(data.files);
|
||||
self.listHelper.resetPage();
|
||||
}
|
||||
|
||||
if (data.job_info !== undefined) {
|
||||
self.job_info(data.job_info);
|
||||
}
|
||||
};
|
||||
|
||||
self.onBeforeBinding = function () {
|
||||
$('#bambu_timelapse').appendTo("#timelapse");
|
||||
};
|
||||
|
||||
self.onAfterBinding = function () {
|
||||
console.log(self.ams_mapping_computed());
|
||||
};
|
||||
|
||||
self.showTimelapseThumbnail = function(data) {
|
||||
$("#bambu_printer_timelapse_thumbnail").attr("src", data.thumbnail);
|
||||
$("#bambu_printer_timelapse_preview").modal('show');
|
||||
};
|
||||
|
||||
self.onBeforePrintStart = function(start_print_command, data) {
|
||||
self.ams_mapping(self.ams_mapping_computed());
|
||||
self.start_print_command = start_print_command;
|
||||
self.use_ams = self.settingsViewModel.settings.plugins.bambu_printer.use_ams();
|
||||
// prevent starting locally stored files, once data is added to core OctoPrint this
|
||||
// could be adjusted to include additional processing like get sliced file's
|
||||
// spool assignments and colors from plate_#.json inside 3mf file.
|
||||
if(data && data.origin !== "sdcard") {
|
||||
return false;
|
||||
}
|
||||
$("#bambu_printer_print_options").modal('show');
|
||||
return false;
|
||||
};
|
||||
|
||||
self.toggle_spool_active = function(data) {
|
||||
if(data.index() >= 0){
|
||||
data.original_index = ko.observable(data.index());
|
||||
data.index(-1);
|
||||
} else {
|
||||
data.index(data.original_index());
|
||||
}
|
||||
};
|
||||
|
||||
self.cancel_print_options = function() {
|
||||
self.settingsViewModel.settings.plugins.bambu_printer.use_ams(self.use_ams);
|
||||
$("#bambu_printer_print_options").modal('hide');
|
||||
};
|
||||
|
||||
self.accept_print_options = function() {
|
||||
console.log("starting print!!!!");
|
||||
console.log(self.ams_mapping());
|
||||
$("#bambu_printer_print_options").modal('hide');
|
||||
var flattened_ams_mapping = ko.utils.arrayMap(self.ams_mapping(), function(item) {
|
||||
return item.index();
|
||||
});
|
||||
self.settingsViewModel.settings.plugins.bambu_printer.ams_mapping(flattened_ams_mapping);
|
||||
self.settingsViewModel.saveData(undefined, self.start_print_command);
|
||||
// self.settingsViewModel.saveData();
|
||||
};
|
||||
}
|
||||
|
||||
/* view model class, parameters for constructor, container to bind to
|
||||
* Please see http://docs.octoprint.org/en/master/plugins/viewmodels.html#registering-custom-viewmodels for more details
|
||||
* and a full list of the available options.
|
||||
*/
|
||||
OCTOPRINT_VIEWMODELS.push({
|
||||
construct: Bambu_printerViewModel,
|
||||
// ViewModels your plugin depends on, e.g. loginStateViewModel, settingsViewModel, ...
|
||||
dependencies: [ /* "loginStateViewModel", "settingsViewModel" */ ],
|
||||
// Elements to bind to, e.g. #settings_plugin_bambu_printer, #tab_plugin_bambu_printer, ...
|
||||
elements: [ /* ... */ ]
|
||||
dependencies: ["settingsViewModel", "filesViewModel", "loginStateViewModel", "accessViewModel", "timelapseViewModel"],
|
||||
elements: ["#bambu_printer_print_options", "#settings_plugin_bambu_printer", "#bambu_timelapse", "#sidebar_plugin_bambu_printer"]
|
||||
});
|
||||
});
|
||||
|
8
octoprint_bambu_printer/static/js/jquery-ui.min.js
vendored
Normal file
8
octoprint_bambu_printer/static/js/jquery-ui.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
490
octoprint_bambu_printer/static/js/knockout-sortable.1.2.0.js
Normal file
490
octoprint_bambu_printer/static/js/knockout-sortable.1.2.0.js
Normal file
@ -0,0 +1,490 @@
|
||||
// knockout-sortable 1.2.0 | (c) 2019 Ryan Niemeyer | http://www.opensource.org/licenses/mit-license
|
||||
;(function(factory) {
|
||||
if (typeof define === "function" && define.amd) {
|
||||
// AMD anonymous module
|
||||
define(["knockout", "jquery", "jquery-ui/ui/widgets/sortable", "jquery-ui/ui/widgets/draggable", "jquery-ui/ui/widgets/droppable"], factory);
|
||||
} else if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
|
||||
// CommonJS module
|
||||
var ko = require("knockout"),
|
||||
jQuery = require("jquery");
|
||||
require("jquery-ui/ui/widgets/sortable");
|
||||
require("jquery-ui/ui/widgets/draggable");
|
||||
require("jquery-ui/ui/widgets/droppable");
|
||||
factory(ko, jQuery);
|
||||
} else {
|
||||
// No module loader (plain <script> tag) - put directly in global namespace
|
||||
factory(window.ko, window.jQuery);
|
||||
}
|
||||
})(function(ko, $) {
|
||||
var ITEMKEY = "ko_sortItem",
|
||||
INDEXKEY = "ko_sourceIndex",
|
||||
LISTKEY = "ko_sortList",
|
||||
PARENTKEY = "ko_parentList",
|
||||
DRAGKEY = "ko_dragItem",
|
||||
unwrap = ko.utils.unwrapObservable,
|
||||
dataGet = ko.utils.domData.get,
|
||||
dataSet = ko.utils.domData.set,
|
||||
version = $.ui && $.ui.version,
|
||||
//1.8.24 included a fix for how events were triggered in nested sortables. indexOf checks will fail if version starts with that value (0 vs. -1)
|
||||
hasNestedSortableFix = version && version.indexOf("1.6.") && version.indexOf("1.7.") && (version.indexOf("1.8.") || version === "1.8.24");
|
||||
|
||||
//internal afterRender that adds meta-data to children
|
||||
var addMetaDataAfterRender = function(elements, data) {
|
||||
ko.utils.arrayForEach(elements, function(element) {
|
||||
if (element.nodeType === 1) {
|
||||
dataSet(element, ITEMKEY, data);
|
||||
dataSet(element, PARENTKEY, dataGet(element.parentNode, LISTKEY));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//prepare the proper options for the template binding
|
||||
var prepareTemplateOptions = function(valueAccessor, dataName) {
|
||||
var result = {},
|
||||
options = {},
|
||||
actualAfterRender;
|
||||
|
||||
//build our options to pass to the template engine
|
||||
if (ko.utils.peekObservable(valueAccessor()).data) {
|
||||
options = unwrap(valueAccessor() || {});
|
||||
result[dataName] = options.data;
|
||||
if (options.hasOwnProperty("template")) {
|
||||
result.name = options.template;
|
||||
}
|
||||
} else {
|
||||
result[dataName] = valueAccessor();
|
||||
}
|
||||
|
||||
ko.utils.arrayForEach(["afterAdd", "afterRender", "as", "beforeRemove", "includeDestroyed", "templateEngine", "templateOptions", "nodes"], function (option) {
|
||||
if (options.hasOwnProperty(option)) {
|
||||
result[option] = options[option];
|
||||
} else if (ko.bindingHandlers.sortable.hasOwnProperty(option)) {
|
||||
result[option] = ko.bindingHandlers.sortable[option];
|
||||
}
|
||||
});
|
||||
|
||||
//use an afterRender function to add meta-data
|
||||
if (dataName === "foreach") {
|
||||
if (result.afterRender) {
|
||||
//wrap the existing function, if it was passed
|
||||
actualAfterRender = result.afterRender;
|
||||
result.afterRender = function(element, data) {
|
||||
addMetaDataAfterRender.call(data, element, data);
|
||||
actualAfterRender.call(data, element, data);
|
||||
};
|
||||
} else {
|
||||
result.afterRender = addMetaDataAfterRender;
|
||||
}
|
||||
}
|
||||
|
||||
//return options to pass to the template binding
|
||||
return result;
|
||||
};
|
||||
|
||||
var updateIndexFromDestroyedItems = function(index, items) {
|
||||
var unwrapped = unwrap(items);
|
||||
|
||||
if (unwrapped) {
|
||||
for (var i = 0; i <= index; i++) {
|
||||
//add one for every destroyed item we find before the targetIndex in the target array
|
||||
if (unwrapped[i] && unwrap(unwrapped[i]._destroy)) {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
};
|
||||
|
||||
//remove problematic leading/trailing whitespace from templates
|
||||
var stripTemplateWhitespace = function(element, name) {
|
||||
var templateSource,
|
||||
templateElement;
|
||||
|
||||
//process named templates
|
||||
if (name) {
|
||||
templateElement = document.getElementById(name);
|
||||
if (templateElement) {
|
||||
templateSource = new ko.templateSources.domElement(templateElement);
|
||||
templateSource.text($.trim(templateSource.text()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
//remove leading/trailing non-elements from anonymous templates
|
||||
$(element).contents().each(function() {
|
||||
if (this && this.nodeType !== 1) {
|
||||
element.removeChild(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//connect items with observableArrays
|
||||
ko.bindingHandlers.sortable = {
|
||||
init: function(element, valueAccessor, allBindingsAccessor, data, context) {
|
||||
var $element = $(element),
|
||||
value = unwrap(valueAccessor()) || {},
|
||||
templateOptions = prepareTemplateOptions(valueAccessor, "foreach"),
|
||||
sortable = {},
|
||||
startActual, updateActual;
|
||||
|
||||
stripTemplateWhitespace(element, templateOptions.name);
|
||||
|
||||
//build a new object that has the global options with overrides from the binding
|
||||
$.extend(true, sortable, ko.bindingHandlers.sortable);
|
||||
if (value.options && sortable.options) {
|
||||
ko.utils.extend(sortable.options, value.options);
|
||||
delete value.options;
|
||||
}
|
||||
ko.utils.extend(sortable, value);
|
||||
|
||||
//if allowDrop is an observable or a function, then execute it in a computed observable
|
||||
if (sortable.connectClass && (ko.isObservable(sortable.allowDrop) || typeof sortable.allowDrop == "function")) {
|
||||
ko.computed({
|
||||
read: function() {
|
||||
var value = unwrap(sortable.allowDrop),
|
||||
shouldAdd = typeof value == "function" ? value.call(this, templateOptions.foreach) : value;
|
||||
ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, shouldAdd);
|
||||
},
|
||||
disposeWhenNodeIsRemoved: element
|
||||
}, this);
|
||||
} else {
|
||||
ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, sortable.allowDrop);
|
||||
}
|
||||
|
||||
//wrap the template binding
|
||||
ko.bindingHandlers.template.init(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
|
||||
|
||||
//keep a reference to start/update functions that might have been passed in
|
||||
startActual = sortable.options.start;
|
||||
updateActual = sortable.options.update;
|
||||
|
||||
//ensure draggable table row cells maintain their width while dragging (unless a helper is provided)
|
||||
if ( !sortable.options.helper ) {
|
||||
sortable.options.helper = function(e, ui) {
|
||||
if (ui.is("tr")) {
|
||||
ui.children().each(function() {
|
||||
$(this).width($(this).width());
|
||||
});
|
||||
}
|
||||
return ui;
|
||||
};
|
||||
}
|
||||
|
||||
//initialize sortable binding after template binding has rendered in update function
|
||||
var createTimeout = setTimeout(function() {
|
||||
var dragItem;
|
||||
var originalReceive = sortable.options.receive;
|
||||
|
||||
$element.sortable(ko.utils.extend(sortable.options, {
|
||||
start: function(event, ui) {
|
||||
//track original index
|
||||
var el = ui.item[0];
|
||||
dataSet(el, INDEXKEY, ko.utils.arrayIndexOf(ui.item.parent().children(), el));
|
||||
|
||||
//make sure that fields have a chance to update model
|
||||
ui.item.find("input:focus").change();
|
||||
if (startActual) {
|
||||
startActual.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
receive: function(event, ui) {
|
||||
//optionally apply an existing receive handler
|
||||
if (typeof originalReceive === "function") {
|
||||
originalReceive.call(this, event, ui);
|
||||
}
|
||||
|
||||
dragItem = dataGet(ui.item[0], DRAGKEY);
|
||||
if (dragItem) {
|
||||
//copy the model item, if a clone option is provided
|
||||
if (dragItem.clone) {
|
||||
dragItem = dragItem.clone();
|
||||
}
|
||||
|
||||
//configure a handler to potentially manipulate item before drop
|
||||
if (sortable.dragged) {
|
||||
dragItem = sortable.dragged.call(this, dragItem, event, ui) || dragItem;
|
||||
}
|
||||
}
|
||||
},
|
||||
update: function(event, ui) {
|
||||
var sourceParent, targetParent, sourceIndex, targetIndex, arg,
|
||||
el = ui.item[0],
|
||||
parentEl = ui.item.parent()[0],
|
||||
item = dataGet(el, ITEMKEY) || dragItem;
|
||||
|
||||
if (!item) {
|
||||
$(el).remove();
|
||||
}
|
||||
dragItem = null;
|
||||
|
||||
//make sure that moves only run once, as update fires on multiple containers
|
||||
if (item && (this === parentEl) || (!hasNestedSortableFix && $.contains(this, parentEl))) {
|
||||
//identify parents
|
||||
sourceParent = dataGet(el, PARENTKEY);
|
||||
sourceIndex = dataGet(el, INDEXKEY);
|
||||
targetParent = dataGet(el.parentNode, LISTKEY);
|
||||
targetIndex = ko.utils.arrayIndexOf(ui.item.parent().children(), el);
|
||||
|
||||
//take destroyed items into consideration
|
||||
if (!templateOptions.includeDestroyed) {
|
||||
sourceIndex = updateIndexFromDestroyedItems(sourceIndex, sourceParent);
|
||||
targetIndex = updateIndexFromDestroyedItems(targetIndex, targetParent);
|
||||
}
|
||||
|
||||
//build up args for the callbacks
|
||||
if (sortable.beforeMove || sortable.afterMove) {
|
||||
arg = {
|
||||
item: item,
|
||||
sourceParent: sourceParent,
|
||||
sourceParentNode: sourceParent && ui.sender || el.parentNode,
|
||||
sourceIndex: sourceIndex,
|
||||
targetParent: targetParent,
|
||||
targetIndex: targetIndex,
|
||||
cancelDrop: false
|
||||
};
|
||||
|
||||
//execute the configured callback prior to actually moving items
|
||||
if (sortable.beforeMove) {
|
||||
sortable.beforeMove.call(this, arg, event, ui);
|
||||
}
|
||||
}
|
||||
|
||||
//call cancel on the correct list, so KO can take care of DOM manipulation
|
||||
if (sourceParent) {
|
||||
$(sourceParent === targetParent ? this : ui.sender || this).sortable("cancel");
|
||||
}
|
||||
//for a draggable item just remove the element
|
||||
else {
|
||||
$(el).remove();
|
||||
}
|
||||
|
||||
//if beforeMove told us to cancel, then we are done
|
||||
if (arg && arg.cancelDrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
//if the strategy option is unset or false, employ the order strategy involving removal and insertion of items
|
||||
if (!sortable.hasOwnProperty("strategyMove") || sortable.strategyMove === false) {
|
||||
//do the actual move
|
||||
if (targetIndex >= 0) {
|
||||
if (sourceParent) {
|
||||
sourceParent.splice(sourceIndex, 1);
|
||||
|
||||
//if using deferred updates plugin, force updates
|
||||
if (ko.processAllDeferredBindingUpdates) {
|
||||
ko.processAllDeferredBindingUpdates();
|
||||
}
|
||||
|
||||
//if using deferred updates on knockout 3.4, force updates
|
||||
if (ko.options && ko.options.deferUpdates) {
|
||||
ko.tasks.runEarly();
|
||||
}
|
||||
}
|
||||
|
||||
targetParent.splice(targetIndex, 0, item);
|
||||
}
|
||||
|
||||
//rendering is handled by manipulating the observableArray; ignore dropped element
|
||||
dataSet(el, ITEMKEY, null);
|
||||
}
|
||||
else { //employ the strategy of moving items
|
||||
if (targetIndex >= 0) {
|
||||
if (sourceParent) {
|
||||
if (sourceParent !== targetParent) {
|
||||
// moving from one list to another
|
||||
|
||||
sourceParent.splice(sourceIndex, 1);
|
||||
targetParent.splice(targetIndex, 0, item);
|
||||
|
||||
//rendering is handled by manipulating the observableArray; ignore dropped element
|
||||
dataSet(el, ITEMKEY, null);
|
||||
ui.item.remove();
|
||||
}
|
||||
else {
|
||||
// moving within same list
|
||||
var underlyingList = unwrap(sourceParent);
|
||||
|
||||
// notify 'beforeChange' subscribers
|
||||
if (sourceParent.valueWillMutate) {
|
||||
sourceParent.valueWillMutate();
|
||||
}
|
||||
|
||||
// move from source index ...
|
||||
underlyingList.splice(sourceIndex, 1);
|
||||
// ... to target index
|
||||
underlyingList.splice(targetIndex, 0, item);
|
||||
|
||||
// notify subscribers
|
||||
if (sourceParent.valueHasMutated) {
|
||||
sourceParent.valueHasMutated();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// drop new element from outside
|
||||
targetParent.splice(targetIndex, 0, item);
|
||||
|
||||
//rendering is handled by manipulating the observableArray; ignore dropped element
|
||||
dataSet(el, ITEMKEY, null);
|
||||
ui.item.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//if using deferred updates plugin, force updates
|
||||
if (ko.processAllDeferredBindingUpdates) {
|
||||
ko.processAllDeferredBindingUpdates();
|
||||
}
|
||||
|
||||
//allow binding to accept a function to execute after moving the item
|
||||
if (sortable.afterMove) {
|
||||
sortable.afterMove.call(this, arg, event, ui);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateActual) {
|
||||
updateActual.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
connectWith: sortable.connectClass ? "." + sortable.connectClass : false
|
||||
}));
|
||||
|
||||
//handle enabling/disabling sorting
|
||||
if (sortable.isEnabled !== undefined) {
|
||||
ko.computed({
|
||||
read: function() {
|
||||
$element.sortable(unwrap(sortable.isEnabled) ? "enable" : "disable");
|
||||
},
|
||||
disposeWhenNodeIsRemoved: element
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
|
||||
//handle disposal
|
||||
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
|
||||
//only call destroy if sortable has been created
|
||||
if ($element.data("ui-sortable") || $element.data("sortable")) {
|
||||
$element.sortable("destroy");
|
||||
}
|
||||
|
||||
ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, false);
|
||||
|
||||
//do not create the sortable if the element has been removed from DOM
|
||||
clearTimeout(createTimeout);
|
||||
});
|
||||
|
||||
return { 'controlsDescendantBindings': true };
|
||||
},
|
||||
update: function(element, valueAccessor, allBindingsAccessor, data, context) {
|
||||
var templateOptions = prepareTemplateOptions(valueAccessor, "foreach");
|
||||
|
||||
//attach meta-data
|
||||
dataSet(element, LISTKEY, templateOptions.foreach);
|
||||
|
||||
//call template binding's update with correct options
|
||||
ko.bindingHandlers.template.update(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
|
||||
},
|
||||
connectClass: 'ko_container',
|
||||
allowDrop: true,
|
||||
afterMove: null,
|
||||
beforeMove: null,
|
||||
options: {}
|
||||
};
|
||||
|
||||
//create a draggable that is appropriate for dropping into a sortable
|
||||
ko.bindingHandlers.draggable = {
|
||||
init: function(element, valueAccessor, allBindingsAccessor, data, context) {
|
||||
var value = unwrap(valueAccessor()) || {},
|
||||
options = value.options || {},
|
||||
draggableOptions = ko.utils.extend({}, ko.bindingHandlers.draggable.options),
|
||||
templateOptions = prepareTemplateOptions(valueAccessor, "data"),
|
||||
connectClass = value.connectClass || ko.bindingHandlers.draggable.connectClass,
|
||||
isEnabled = value.isEnabled !== undefined ? value.isEnabled : ko.bindingHandlers.draggable.isEnabled;
|
||||
|
||||
value = "data" in value ? value.data : value;
|
||||
|
||||
//set meta-data
|
||||
dataSet(element, DRAGKEY, value);
|
||||
|
||||
//override global options with override options passed in
|
||||
ko.utils.extend(draggableOptions, options);
|
||||
|
||||
//setup connection to a sortable
|
||||
draggableOptions.connectToSortable = connectClass ? "." + connectClass : false;
|
||||
|
||||
//initialize draggable
|
||||
$(element).draggable(draggableOptions);
|
||||
|
||||
//handle enabling/disabling sorting
|
||||
if (isEnabled !== undefined) {
|
||||
ko.computed({
|
||||
read: function() {
|
||||
$(element).draggable(unwrap(isEnabled) ? "enable" : "disable");
|
||||
},
|
||||
disposeWhenNodeIsRemoved: element
|
||||
});
|
||||
}
|
||||
|
||||
//handle disposal
|
||||
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
|
||||
$(element).draggable("destroy");
|
||||
});
|
||||
|
||||
return ko.bindingHandlers.template.init(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
|
||||
},
|
||||
update: function(element, valueAccessor, allBindingsAccessor, data, context) {
|
||||
var templateOptions = prepareTemplateOptions(valueAccessor, "data");
|
||||
|
||||
return ko.bindingHandlers.template.update(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
|
||||
},
|
||||
connectClass: ko.bindingHandlers.sortable.connectClass,
|
||||
options: {
|
||||
helper: "clone"
|
||||
}
|
||||
};
|
||||
|
||||
// Simple Droppable Implementation
|
||||
// binding that updates (function or observable)
|
||||
ko.bindingHandlers.droppable = {
|
||||
init: function(element, valueAccessor, allBindingsAccessor, data, context) {
|
||||
var value = unwrap(valueAccessor()) || {},
|
||||
options = value.options || {},
|
||||
droppableOptions = ko.utils.extend({}, ko.bindingHandlers.droppable.options),
|
||||
isEnabled = value.isEnabled !== undefined ? value.isEnabled : ko.bindingHandlers.droppable.isEnabled;
|
||||
|
||||
//override global options with override options passed in
|
||||
ko.utils.extend(droppableOptions, options);
|
||||
|
||||
//get reference to drop method
|
||||
value = "data" in value ? value.data : valueAccessor();
|
||||
|
||||
//set drop method
|
||||
droppableOptions.drop = function(event, ui) {
|
||||
var droppedItem = dataGet(ui.draggable[0], DRAGKEY) || dataGet(ui.draggable[0], ITEMKEY);
|
||||
value(droppedItem);
|
||||
};
|
||||
|
||||
//initialize droppable
|
||||
$(element).droppable(droppableOptions);
|
||||
|
||||
//handle enabling/disabling droppable
|
||||
if (isEnabled !== undefined) {
|
||||
ko.computed({
|
||||
read: function() {
|
||||
$(element).droppable(unwrap(isEnabled) ? "enable": "disable");
|
||||
},
|
||||
disposeWhenNodeIsRemoved: element
|
||||
});
|
||||
}
|
||||
|
||||
//handle disposal
|
||||
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
|
||||
$(element).droppable("destroy");
|
||||
});
|
||||
},
|
||||
options: {
|
||||
accept: "*"
|
||||
}
|
||||
};
|
||||
});
|
31
octoprint_bambu_printer/templates/bambu_printer.jinja2
Normal file
31
octoprint_bambu_printer/templates/bambu_printer.jinja2
Normal file
@ -0,0 +1,31 @@
|
||||
<div id="bambu_printer_print_options" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<h3>{{ _('Bambu Print Options') }}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row-fluid">
|
||||
<div class="span6">
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.timelapse"> {{ _('Enable timelapse') }}</label>
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.bed_leveling"> {{ _('Enable bed leveling') }}</label>
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.flow_cali"> {{ _('Enable flow calibration') }}</label>
|
||||
</div>
|
||||
<div class="span6">
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.vibration_cali"> {{ _('Enable vibration calibration') }}</label>
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.layer_inspect"> {{ _('Enable first layer inspection') }}</label>
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.use_ams"> {{ _('Use AMS') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid" data-bind="visible: settingsViewModel.settings.plugins.bambu_printer.use_ams">
|
||||
{{ _('Filament Assighnment') }}: {{ _('Click') }} <a href="#">{{ _('here') }}</a> {{ _('for usage details.') }}
|
||||
</div>
|
||||
<div class="row-fluid" data-bind="visible: settingsViewModel.settings.plugins.bambu_printer.use_ams, sortable: {data: ams_mapping, options: {cancel: '.unsortable'}}">
|
||||
<div class="btn" data-bind="attr: {title: name}, event: {dblclick: $root.toggle_spool_active}, css: {disabled: (index()<0)}">
|
||||
<i class="fa fa-2x fa-dot-circle" data-bind="css: {'fas': !empty(), 'far': empty()}, style: {'color': ('#'+color())}"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-danger" data-bind="click: cancel_print_options">{{ _('Cancel') }}</button>
|
||||
<button class="btn btn-primary" data-bind="click: accept_print_options">{{ _('Print') }}</button>
|
||||
</div>
|
||||
</div>
|
@ -1,40 +1,87 @@
|
||||
<h3>Virtual Printer</h3>
|
||||
<h3>Bambu Printer Settings <small>{{ _('Version') }} {{ plugin_bambu_printer_plugin_version }}</small></h3>
|
||||
|
||||
<form class="form-horizontal" onsubmit="return false;">
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Device Type') }}</label>
|
||||
<div class="controls">
|
||||
<select class="input-block-level" data-bind="options: ['A1', 'A1MINI', 'P1P', 'P1S', 'X1', 'X1C'], value: settings.plugins.bambu_printer.device_type, allowUnset: true">
|
||||
<select class="input-block-level" data-bind="options: ['A1', 'A1MINI', 'P1P', 'P1S', 'X1', 'X1C'], value: settingsViewModel.settings.plugins.bambu_printer.device_type, allowUnset: true">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('IP Address') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: settings.plugins.bambu_printer.host" placeholder="192.168.0.2" title="{{ _('IP address or hostname of the printer') }}"></input>
|
||||
<input type="text" class="input-block-level" data-bind="value: settingsViewModel.settings.plugins.bambu_printer.host" placeholder="192.168.0.2" title="{{ _('IP address or hostname of the printer') }}"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Serial Number') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: settings.plugins.bambu_printer.serial" title="{{ _('Serial number of printer') }}"></input>
|
||||
<input type="text" class="input-block-level" data-bind="value: settingsViewModel.settings.plugins.bambu_printer.serial" title="{{ _('Serial number of printer') }}"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Access Code') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: settings.plugins.bambu_printer.access_code" title="{{ _('Access code of printer') }}"></input>
|
||||
<input type="text" class="input-block-level" data-bind="value: settingsViewModel.settings.plugins.bambu_printer.access_code" title="{{ _('Access code of printer') }}"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Print Options') }}</label>
|
||||
<div class="controls">
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.timelapse"> {{ _('Enable timelapse') }}</label>
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.bed_leveling"> {{ _('Enable bed leveling') }}</label>
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.flow_cali"> {{ _('Enable flow calibration') }}</label>
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.vibration_cali"> {{ _('Enable vibration calibration') }}</label>
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.layer_inspect"> {{ _('Enable first layer inspection') }}</label>
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.use_ams"> {{ _('Use AMS') }}</label>
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.local_mqtt"> {{ _('Use Local Access, disable for cloud connection') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="visible: !settingsViewModel.settings.plugins.bambu_printer.local_mqtt()">
|
||||
<label class="control-label">{{ _('Region') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: settingsViewModel.settings.plugins.bambu_printer.region" title="{{ _('Region used to connect, ie China, US') }}"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="visible: !settingsViewModel.settings.plugins.bambu_printer.local_mqtt()">
|
||||
<label class="control-label">{{ _('Email') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: settingsViewModel.settings.plugins.bambu_printer.email" title="{{ _('Registered email address') }}" autocomplete="off"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="visible: !settingsViewModel.settings.plugins.bambu_printer.local_mqtt() && show_password()">
|
||||
<label class="control-label">{{ _('Password') }}</label>
|
||||
<div class="controls">
|
||||
<div class="input-block-level input-append" data-bind="css: {'input-append': !show_verification()}">
|
||||
<input id="bambu_cloud_password" type="password" class="input-text input-block-level" title="{{ _('Password to generate verification code') }}" autocomplete="new-password"></input>
|
||||
<span class="btn btn-primary add-on" data-bind="visible: !show_verification(), click: getAuthToken">{{ _('Login') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="visible: show_verification()">
|
||||
<label class="control-label">{{ _('Verify') }}</label>
|
||||
<div class="controls">
|
||||
<div class="input-block-level input-append">
|
||||
<input id="bambu_cloud_verify_code" type="password" class="input-text input-block-level" title="{{ _('Verification code to generate auth token') }}"></input>
|
||||
<span class="btn btn-primary add-on" data-bind="click: verifyCode">{{ _('Verify') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="visible: !settingsViewModel.settings.plugins.bambu_printer.local_mqtt()">
|
||||
<label class="control-label">{{ _('Auth Token') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: settingsViewModel.settings.plugins.bambu_printer.auth_token" title="{{ _('Auth Token') }}"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Default Print Options') }}</label>
|
||||
<div class="controls">
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.timelapse"> {{ _('Enable timelapse') }}</label>
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.bed_leveling"> {{ _('Enable bed leveling') }}</label>
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.flow_cali"> {{ _('Enable flow calibration') }}</label>
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.vibration_cali"> {{ _('Enable vibration calibration') }}</label>
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.layer_inspect"> {{ _('Enable first layer inspection') }}</label>
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.use_ams"> {{ _('Use AMS') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
{#<div class="control-group">
|
||||
<label class="control-label">{{ _('Always Use Default') }}</label>
|
||||
<div class="controls">
|
||||
<label class="checkbox"><input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.always_use_default_options"> </label>
|
||||
</div>
|
||||
</div>#}
|
||||
</form>
|
||||
|
@ -0,0 +1,15 @@
|
||||
<div class="row-fluid" data-bind="foreach: {data: settingsViewModel.settings.plugins.bambu_printer.ams_data, as: 'ams'}">
|
||||
<!-- ko if: $data -->
|
||||
<div class="well" data-bind="foreach: tray">
|
||||
<div class="span3 text-center" data-bind="attr: {title: name}">
|
||||
<div class="row-fluid" data-bind="css: {'active': ($root.settingsViewModel.settings.plugins.bambu_printer.ams_current_tray() == (($parentContext.$index() * 4) + $index()))}">
|
||||
<i class="fa fa-3x fa-dot-circle" data-bind="css: {'fas': !empty(), 'far': empty()}, style: {'color': ('#'+color())}"></i><br>
|
||||
<div class="text-center" data-bind="text: type"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
<div class="row-fluid" data-bind="visible: job_info">
|
||||
<div class="span6">{{ _('Layer') }}:</div><div class="span6" data-bind="text: function(){return (job_info.current_layer() + ' of ' + job_info.total_layers);}"></div>
|
||||
</div>
|
71
octoprint_bambu_printer/templates/bambu_timelapse.jinja2
Normal file
71
octoprint_bambu_printer/templates/bambu_timelapse.jinja2
Normal file
@ -0,0 +1,71 @@
|
||||
<div class="row-fluid" id="bambu_timelapse">
|
||||
<h1>{{ _('Bambu Timelapses') }}</h1>
|
||||
|
||||
<div class="pull-right">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="fas fa-wrench"></i> <span class="caret"></span></button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="javascript:void(0)" data-bind="click: function() { listHelper.changeSorting('name'); }"><i class="fas fa-check" data-bind="style: {visibility: listHelper.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> {{ _('Sort by name') }} ({{ _('ascending') }})</a></li>
|
||||
<li><a href="javascript:void(0)" data-bind="click: function() { listHelper.changeSorting('date'); }"><i class="fas fa-check" data-bind="style: {visibility: listHelper.currentSorting() == 'date' ? 'visible' : 'hidden'}"></i> {{ _('Sort by date') }} ({{ _('descending') }})</a></li>
|
||||
<li><a href="javascript:void(0)" data-bind="click: function() { listHelper.changeSorting('size'); }"><i class="fas fa-check" data-bind="style: {visibility: listHelper.currentSorting() == 'size' ? 'visible' : 'hidden'}"></i> {{ _('Sort by file size') }} ({{ _('descending') }})</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover table-condensed table-hover" id="bambu_timelapse_files">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="timelapse_files_thumb"></th>
|
||||
<th class="timelapse_files_details">{{ _('Details') }}</th>
|
||||
<th class="timelapse_files_action">{{ _('Action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: listHelper.paginatedItems">
|
||||
<tr data-bind="attr: {title: name}">
|
||||
<td class="timelapse_files_thumb">
|
||||
<div class="thumb" data-bind="css: { letterbox: $data.thumbnail }">
|
||||
<!-- ko if: $data.thumbnail -->
|
||||
<img data-bind="attr:{src: thumbnail}" loading="lazy" style="aspect-ratio: 3 / 2;"/>
|
||||
<!-- /ko -->
|
||||
<a href="javascript:void(0)" data-bind="css: {disabled: !$root.timelapseViewModel.isTimelapseViewable($data)}, click: $root.showTimelapseThumbnail"></a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="timelapse_files_details">
|
||||
<p class="name" data-bind="text: name"></p>
|
||||
<p class="detail">{{ _('Recorded:') }} <span data-bind="text: formatTimeAgo(timestamp)"/></p>
|
||||
<p class="detail">{{ _('Size:') }} <span data-bind="text: size"/></p>
|
||||
</td>
|
||||
<td class="timelapse_files_action">
|
||||
<div class="btn-group action-buttons">
|
||||
<a href="javascript:void(0)" class="btn btn-mini" data-bind="css: {disabled: !$root.loginStateViewModel.hasPermissionKo($root.accessViewModel.permissions.TIMELAPSE_DOWNLOAD)()}, attr: { href: ($root.loginStateViewModel.hasPermission($root.accessViewModel.permissions.TIMELAPSE_DOWNLOAD)) ? $data.url : 'javascript:void(0)' }"><i class="fas fa-download"></i></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination pagination-mini pagination-centered">
|
||||
<ul>
|
||||
<li data-bind="css: {disabled: listHelper.currentPage() === 0}"><a href="javascript:void(0)" data-bind="click: listHelper.prevPage">«</a></li>
|
||||
</ul>
|
||||
<ul data-bind="foreach: listHelper.pages">
|
||||
<li data-bind="css: { active: $data.number === $root.listHelper.currentPage(), disabled: $data.number === -1 }"><a href="javascript:void(0)" data-bind="text: $data.text, click: function() { $root.listHelper.changePage($data.number); }"></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li data-bind="css: {disabled: listHelper.currentPage() === listHelper.lastPage()}"><a href="javascript:void(0)" data-bind="click: listHelper.nextPage">»</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="bambu_printer_timelapse_preview" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">×</a>
|
||||
<h3>{{ _('Timelapse Thumbnail') }}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row-fluid">
|
||||
<img id="bambu_printer_timelapse_thumbnail" src="" class="row-fluid" style="aspect-ratio: 3 / 2;"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Close') }}</a>
|
||||
</div>
|
||||
</div>
|
@ -1,994 +0,0 @@
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html"
|
||||
|
||||
|
||||
import collections
|
||||
import datetime
|
||||
import os
|
||||
import queue
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
import asyncio
|
||||
from pybambu import BambuClient, commands
|
||||
|
||||
from serial import SerialTimeoutException
|
||||
from octoprint.util import RepeatedTimer, to_bytes, to_unicode, get_dos_filename
|
||||
from octoprint.util.files import unix_timestamp_to_m20_timestamp
|
||||
|
||||
from .ftpsclient import IoTFTPSClient
|
||||
|
||||
|
||||
# noinspection PyBroadException
|
||||
class BambuPrinter:
|
||||
command_regex = re.compile(r"^([GM])(\d+)")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
settings,
|
||||
printer_profile_manager,
|
||||
data_folder,
|
||||
seriallog_handler=None,
|
||||
read_timeout=5.0,
|
||||
write_timeout=10.0,
|
||||
faked_baudrate=115200,
|
||||
):
|
||||
self._busyInterval = 2.0
|
||||
self.tick_rate = 2.0
|
||||
self._errors = {
|
||||
"checksum_mismatch": "Checksum mismatch",
|
||||
"checksum_missing": "Missing checksum",
|
||||
"lineno_mismatch": "expected line {} got {}",
|
||||
"lineno_missing": "No Line Number with checksum, Last Line: {}",
|
||||
"maxtemp": "MAXTEMP triggered!",
|
||||
"mintemp": "MINTEMP triggered!",
|
||||
"command_unknown": "Unknown command {}",
|
||||
}
|
||||
self._sendBusy = False
|
||||
self._ambient_temperature = 21.3
|
||||
self.temp = [self._ambient_temperature]
|
||||
self.targetTemp = [0.0]
|
||||
self.bedTemp = self._ambient_temperature
|
||||
self.bedTargetTemp = 0.0
|
||||
self._hasChamber = printer_profile_manager.get_current().get("heatedChamber")
|
||||
self.chamberTemp = self._ambient_temperature
|
||||
self.chamberTargetTemp = 0.0
|
||||
self.lastTempAt = time.monotonic()
|
||||
self._firmwareName = "Bambu"
|
||||
self._m115FormatString = "FIRMWARE_NAME:{firmware_name} PROTOCOL_VERSION:1.0"
|
||||
self._received_lines = 0
|
||||
self.extruderCount = 1
|
||||
self._waitInterval = 5.0
|
||||
self._killed = False
|
||||
self._heatingUp = False
|
||||
self.current_line = 0
|
||||
self._writingToSd = False
|
||||
|
||||
self._sdCardReady = True
|
||||
self._sdPrinter = None
|
||||
self._sdPrinting = False
|
||||
self._sdPrintingSemaphore = threading.Event()
|
||||
self._sdPrintingPausedSemaphore = threading.Event()
|
||||
self._selectedSdFile = None
|
||||
self._selectedSdFileSize = 0
|
||||
self._selectedSdFilePos = 0
|
||||
|
||||
self._busy = None
|
||||
self._busy_loop = None
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
self._logger = logging.getLogger(
|
||||
"octoprint.plugins.bambu_printer.BambuPrinter"
|
||||
)
|
||||
|
||||
self._settings = settings
|
||||
self._printer_profile_manager = printer_profile_manager
|
||||
self._faked_baudrate = faked_baudrate
|
||||
self._plugin_data_folder = data_folder
|
||||
|
||||
self._seriallog = logging.getLogger(
|
||||
"octoprint.plugins.bambu_printer.BambuPrinter.serial"
|
||||
)
|
||||
self._seriallog.setLevel(logging.CRITICAL)
|
||||
self._seriallog.propagate = False
|
||||
|
||||
if seriallog_handler is not None:
|
||||
import logging.handlers
|
||||
|
||||
self._seriallog.addHandler(seriallog_handler)
|
||||
self._seriallog.setLevel(logging.INFO)
|
||||
|
||||
self._seriallog.debug("-" * 78)
|
||||
|
||||
self._read_timeout = read_timeout
|
||||
self._write_timeout = write_timeout
|
||||
|
||||
self._rx_buffer_size = 64
|
||||
self._incoming_lock = threading.RLock()
|
||||
|
||||
self.incoming = CharCountingQueue(self._rx_buffer_size, name="RxBuffer")
|
||||
self.outgoing = queue.Queue()
|
||||
self.buffered = queue.Queue(maxsize=4)
|
||||
|
||||
self._last_hms_errors = None
|
||||
|
||||
self.bambu = None
|
||||
|
||||
readThread = threading.Thread(
|
||||
target=self._processIncoming,
|
||||
name="octoprint.plugins.bambu_printer.wait_thread",
|
||||
daemon=True
|
||||
)
|
||||
readThread.start()
|
||||
|
||||
# bufferThread = threading.Thread(
|
||||
# target=self._processBuffer,
|
||||
# name="octoprint.plugins.bambu_printer.buffer_thread",
|
||||
# daemon=True
|
||||
# )
|
||||
# bufferThread.start()
|
||||
|
||||
# Move this into M110 command response?
|
||||
connectionThread = threading.Thread(
|
||||
target=self._create_connection,
|
||||
name="octoprint.plugins.bambu_printer.connection_thread",
|
||||
daemon=True
|
||||
)
|
||||
connectionThread.start()
|
||||
|
||||
def new_update(self, event_type):
|
||||
if event_type == "event_hms_errors":
|
||||
bambu_printer = self.bambu.get_device()
|
||||
if bambu_printer.hms.errors != self._last_hms_errors and bambu_printer.hms.errors["Count"] > 0:
|
||||
self._logger.debug(f"HMS Error: {bambu_printer.hms.errors}")
|
||||
for n in range(1, bambu_printer.hms.errors["Count"]+1):
|
||||
error = bambu_printer.hms.errors[f"{n}-Error"].strip()
|
||||
self._send(f"// action:notification {error}")
|
||||
self._last_hms_errors = bambu_printer.hms.errors
|
||||
elif event_type == "event_printer_data_update":
|
||||
device_data = self.bambu.get_device()
|
||||
ams = device_data.ams.__dict__
|
||||
print_job = device_data.print_job.__dict__
|
||||
temperatures = device_data.temperature.__dict__
|
||||
lights = device_data.lights.__dict__
|
||||
fans = device_data.fans.__dict__
|
||||
speed = device_data.speed.__dict__
|
||||
|
||||
self.temp[0] = temperatures.get("nozzle_temp", 0.0)
|
||||
self.targetTemp[0] = temperatures.get("target_nozzle_temp", 0.0)
|
||||
self.bedTemp = temperatures.get("bed_temp", 0.0)
|
||||
self.bedTargetTemp = temperatures.get("target_bed_temp", 0.0)
|
||||
self.chamberTemp = temperatures.get("chamber_temp", 0.0)
|
||||
|
||||
if print_job.get("gcode_state") == "RUNNING":
|
||||
if not self._sdPrintingSemaphore.is_set():
|
||||
self._sdPrintingSemaphore.set()
|
||||
if self._sdPrintingPausedSemaphore.is_set():
|
||||
self._sdPrintingPausedSemaphore.clear()
|
||||
if not self._sdPrinting:
|
||||
filename = print_job.get("subtask_name")
|
||||
# TODO: swap this out to use 8 dot 3 name based on long name/path
|
||||
self._selectSdFile(filename)
|
||||
self._startSdPrint(from_printer=True)
|
||||
|
||||
# fuzzy math here to get print percentage to match BambuStudio
|
||||
self._selectedSdFilePos = int(self._selectedSdFileSize * ((print_job.get("print_percentage") + 1)/100))
|
||||
|
||||
if print_job.get("gcode_state") == "PAUSE":
|
||||
if not self._sdPrintingPausedSemaphore.is_set():
|
||||
self._sdPrintingPausedSemaphore.set()
|
||||
if self._sdPrintingSemaphore.is_set():
|
||||
self._sdPrintingSemaphore.clear()
|
||||
self._send("// action:paused")
|
||||
self._sendPaused()
|
||||
|
||||
if print_job.get("gcode_state") == "FINISH" and self._sdPrintingSemaphore.is_set():
|
||||
self._selectedSdFilePos = self._selectedSdFileSize
|
||||
self._finishSdPrint()
|
||||
def _create_connection(self):
|
||||
if (self._settings.get(["device_type"]) != "" and
|
||||
self._settings.get(["serial"]) != "" and
|
||||
self._settings.get(["serial"]) != "" and
|
||||
self._settings.get(["username"]) != "" and
|
||||
self._settings.get(["access_code"]) != ""
|
||||
):
|
||||
asyncio.run(self._create_connection_async())
|
||||
|
||||
async def _create_connection_async(self):
|
||||
self.bambu = BambuClient(device_type=self._settings.get(["device_type"]),
|
||||
serial=self._settings.get(["serial"]),
|
||||
host=self._settings.get(["host"]),
|
||||
username=self._settings.get(["username"]),
|
||||
access_code=self._settings.get(["access_code"]),
|
||||
local_mqtt=self._settings.get_boolean(["local_mqtt"]),
|
||||
region=self._settings.get(["region"]),
|
||||
email=self._settings.get(["email"]),
|
||||
auth_token=self._settings.get(["auth_token"])
|
||||
)
|
||||
|
||||
self.bambu.connect(callback=self.new_update)
|
||||
self._logger.info(f"bambu connection status: {self.bambu.connected}")
|
||||
self._sendOk()
|
||||
# while True:
|
||||
# await asyncio.sleep(self.tick_rate)
|
||||
# self._processTemperatureQuery()
|
||||
|
||||
def __str__(self):
|
||||
return "BAMBU(read_timeout={read_timeout},write_timeout={write_timeout},options={options})".format(
|
||||
read_timeout=self._read_timeout,
|
||||
write_timeout=self._write_timeout,
|
||||
options={"device_type": self._settings.get(["device_type"]), "host": self._settings.get(["host"])},
|
||||
)
|
||||
|
||||
def _calculate_resend_every_n(self, resend_ratio):
|
||||
self._resend_every_n = (100 // resend_ratio) if resend_ratio else 0
|
||||
|
||||
def _reset(self):
|
||||
with self._incoming_lock:
|
||||
self._relative = True
|
||||
self._lastX = 0.0
|
||||
self._lastY = 0.0
|
||||
self._lastZ = 0.0
|
||||
self._lastE = [0.0] * self.extruderCount
|
||||
self._lastF = 200
|
||||
|
||||
self._unitModifier = 1
|
||||
self._feedrate_multiplier = 100
|
||||
self._flowrate_multiplier = 100
|
||||
|
||||
self._sdCardReady = True
|
||||
self._sdPrinting = False
|
||||
if self._sdPrinter:
|
||||
self._sdPrinting = False
|
||||
self._sdPrintingSemaphore.clear()
|
||||
self._sdPrintingPausedSemaphore.clear()
|
||||
self._sdPrinter = None
|
||||
self._selectedSdFile = None
|
||||
self._selectedSdFileSize = None
|
||||
self._selectedSdFilePos = None
|
||||
|
||||
if self._writingToSdHandle:
|
||||
try:
|
||||
self._writingToSdHandle.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._writingToSd = False
|
||||
self._writingToSdHandle = None
|
||||
self._writingToSdFile = None
|
||||
self._newSdFilePos = None
|
||||
|
||||
self._heatingUp = False
|
||||
|
||||
self.current_line = 0
|
||||
self.lastN = 0
|
||||
|
||||
self._debug_awol = False
|
||||
self._debug_sleep = 0
|
||||
# self._sleepAfterNext.clear()
|
||||
# self._sleepAfter.clear()
|
||||
|
||||
self._dont_answer = False
|
||||
self._broken_klipper_connection = False
|
||||
|
||||
self._debug_drop_connection = False
|
||||
|
||||
self._killed = False
|
||||
|
||||
if self._sdstatus_reporter is not None:
|
||||
self._sdstatus_reporter.cancel()
|
||||
self._sdstatus_reporter = None
|
||||
|
||||
self._clearQueue(self.incoming)
|
||||
self._clearQueue(self.outgoing)
|
||||
# self._clearQueue(self.buffered)
|
||||
|
||||
if self._settings.get_boolean(["simulateReset"]):
|
||||
for item in self._settings.get(["resetLines"]):
|
||||
self._send(item + "\n")
|
||||
|
||||
self._locked = self._settings.get_boolean(["locked"])
|
||||
|
||||
@property
|
||||
def timeout(self):
|
||||
return self._read_timeout
|
||||
|
||||
@timeout.setter
|
||||
def timeout(self, value):
|
||||
self._logger.debug(f"Setting read timeout to {value}s")
|
||||
self._read_timeout = value
|
||||
|
||||
@property
|
||||
def write_timeout(self):
|
||||
return self._write_timeout
|
||||
|
||||
@write_timeout.setter
|
||||
def write_timeout(self, value):
|
||||
self._logger.debug(f"Setting write timeout to {value}s")
|
||||
self._write_timeout = value
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
return "BAMBU"
|
||||
|
||||
@property
|
||||
def baudrate(self):
|
||||
return self._faked_baudrate
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def _clearQueue(self, q):
|
||||
try:
|
||||
while q.get(block=False):
|
||||
q.task_done()
|
||||
continue
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
def _processIncoming(self):
|
||||
linenumber = 0
|
||||
next_wait_timeout = 0
|
||||
|
||||
def recalculate_next_wait_timeout():
|
||||
nonlocal next_wait_timeout
|
||||
next_wait_timeout = time.monotonic() + self._waitInterval
|
||||
|
||||
recalculate_next_wait_timeout()
|
||||
|
||||
data = None
|
||||
|
||||
buf = b""
|
||||
while self.incoming is not None and not self._killed:
|
||||
try:
|
||||
data = self.incoming.get(timeout=0.01)
|
||||
data = to_bytes(data, encoding="ascii", errors="replace")
|
||||
self.incoming.task_done()
|
||||
except queue.Empty:
|
||||
continue
|
||||
except Exception:
|
||||
if self.incoming is None:
|
||||
# just got closed
|
||||
break
|
||||
|
||||
if data is not None:
|
||||
buf += data
|
||||
nl = buf.find(b"\n") + 1
|
||||
if nl > 0:
|
||||
data = buf[:nl]
|
||||
buf = buf[nl:]
|
||||
else:
|
||||
continue
|
||||
|
||||
recalculate_next_wait_timeout()
|
||||
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
self._received_lines += 1
|
||||
|
||||
# strip checksum
|
||||
if b"*" in data:
|
||||
checksum = int(data[data.rfind(b"*") + 1 :])
|
||||
data = data[: data.rfind(b"*")]
|
||||
if not checksum == self._calculate_checksum(data):
|
||||
self._triggerResend(expected=self.current_line + 1)
|
||||
continue
|
||||
|
||||
self.current_line += 1
|
||||
elif self._settings.get_boolean(["forceChecksum"]):
|
||||
self._send(self._error("checksum_missing"))
|
||||
continue
|
||||
|
||||
# track N = N + 1
|
||||
if data.startswith(b"N") and b"M110" in data:
|
||||
linenumber = int(re.search(b"N([0-9]+)", data).group(1))
|
||||
self.lastN = linenumber
|
||||
self.current_line = linenumber
|
||||
self._sendOk()
|
||||
continue
|
||||
|
||||
elif data.startswith(b"N"):
|
||||
linenumber = int(re.search(b"N([0-9]+)", data).group(1))
|
||||
expected = self.lastN + 1
|
||||
if linenumber != expected:
|
||||
self._triggerResend(actual=linenumber)
|
||||
continue
|
||||
else:
|
||||
self.lastN = linenumber
|
||||
|
||||
data = data.split(None, 1)[1].strip()
|
||||
|
||||
data += b"\n"
|
||||
|
||||
data = to_unicode(data, encoding="ascii", errors="replace").strip()
|
||||
|
||||
# actual command handling
|
||||
command_match = BambuPrinter.command_regex.match(data)
|
||||
if command_match is not None:
|
||||
command = command_match.group(0)
|
||||
letter = command_match.group(1)
|
||||
|
||||
try:
|
||||
# if we have a method _gcode_G, _gcode_M or _gcode_T, execute that first
|
||||
letter_handler = f"_gcode_{letter}"
|
||||
if hasattr(self, letter_handler):
|
||||
code = command_match.group(2)
|
||||
handled = getattr(self, letter_handler)(code, data)
|
||||
if handled:
|
||||
self._sendOk()
|
||||
continue
|
||||
|
||||
# then look for a method _gcode_<command> and execute that if it exists
|
||||
command_handler = f"_gcode_{command}"
|
||||
if hasattr(self, command_handler):
|
||||
handled = getattr(self, command_handler)(data)
|
||||
if handled:
|
||||
self._sendOk()
|
||||
continue
|
||||
else:
|
||||
self._sendOk()
|
||||
|
||||
finally:
|
||||
self._logger.debug(f"{data}")
|
||||
|
||||
self._logger.debug("Closing down read loop")
|
||||
|
||||
##~~ command implementations
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def _gcode_M20(self, data: str) -> bool:
|
||||
if self._sdCardReady:
|
||||
self._listSd(incl_long="L" in data, incl_timestamp="T" in data)
|
||||
return True
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def _gcode_M21(self, data: str) -> bool:
|
||||
self._sdCardReady = True
|
||||
self._send("SD card ok")
|
||||
return True
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def _gcode_M22(self, data: str) -> bool:
|
||||
self._logger.debug("ignoring M22 command.")
|
||||
self._send("M22 disabled for Bambu")
|
||||
return True
|
||||
|
||||
def _gcode_M23(self, data: str) -> bool:
|
||||
if self._sdCardReady:
|
||||
filename = data.split(None, 1)[1].strip()
|
||||
self._selectSdFile(filename)
|
||||
return True
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def _gcode_M24(self, data: str) -> bool:
|
||||
if self._sdCardReady:
|
||||
self._startSdPrint()
|
||||
return True
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def _gcode_M25(self, data: str) -> bool:
|
||||
if self._sdCardReady:
|
||||
self._pauseSdPrint()
|
||||
return True
|
||||
|
||||
def _gcode_M524(self, data: str) -> bool:
|
||||
if self._sdCardReady:
|
||||
self._cancelSdPrint()
|
||||
return False
|
||||
|
||||
def _gcode_M26(self, data: str) -> bool:
|
||||
self._logger.debug("ignoring M26 command.")
|
||||
self._send("M26 disabled for Bambu")
|
||||
return True
|
||||
|
||||
def _gcode_M27(self, data: str) -> bool:
|
||||
def report():
|
||||
if self._sdCardReady:
|
||||
self._reportSdStatus()
|
||||
|
||||
matchS = re.search(r"S([0-9]+)", data)
|
||||
if matchS:
|
||||
interval = int(matchS.group(1))
|
||||
if self._sdstatus_reporter is not None:
|
||||
self._sdstatus_reporter.cancel()
|
||||
|
||||
if interval > 0:
|
||||
self._sdstatus_reporter = RepeatedTimer(interval, report)
|
||||
self._sdstatus_reporter.start()
|
||||
else:
|
||||
self._sdstatus_reporter = None
|
||||
|
||||
report()
|
||||
return True
|
||||
|
||||
def _gcode_M28(self, data: str) -> bool:
|
||||
self._logger.debug("ignoring M28 command.")
|
||||
self._send("M28 disabled for Bambu")
|
||||
return True
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def _gcode_M29(self, data: str) -> bool:
|
||||
self._logger.debug("ignoring M28 command.")
|
||||
self._send("M28 disabled for Bambu")
|
||||
return True
|
||||
|
||||
def _gcode_M30(self, data: str) -> bool:
|
||||
if self._sdCardReady:
|
||||
filename = data.split(None, 1)[1].strip()
|
||||
self._deleteSdFile(filename)
|
||||
return True
|
||||
|
||||
def _gcode_M33(self, data: str) -> bool:
|
||||
self._logger.debug("ignoring M33 command.")
|
||||
self._send("M33 disabled for Bambu")
|
||||
return True
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def _gcode_M105(self, data: str) -> bool:
|
||||
self._processTemperatureQuery()
|
||||
return True
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def _gcode_M115(self, data: str) -> bool:
|
||||
self._send("Bambu Printer Integration")
|
||||
self._send("Cap:EXTENDED_M20:1")
|
||||
self._send("Cap:LFN_WRITE:1")
|
||||
self._send("Cap:LFN_WRITE:1")
|
||||
return True
|
||||
|
||||
def _gcode_M117(self, data: str) -> bool:
|
||||
# we'll just use this to echo a message, to allow playing around with pause triggers
|
||||
result = re.search(r"M117\s+(.*)", data).group(1)
|
||||
self._send(f"echo:{result}")
|
||||
return False
|
||||
|
||||
def _gcode_M118(self, data: str) -> bool:
|
||||
match = re.search(r"M118 (?:(?P<parameter>A1|E1|Pn[012])\s)?(?P<text>.*)", data)
|
||||
if not match:
|
||||
self._send("Unrecognized command parameters for M118")
|
||||
else:
|
||||
result = match.groupdict()
|
||||
text = result["text"]
|
||||
parameter = result["parameter"]
|
||||
|
||||
if parameter == "A1":
|
||||
self._send(f"//{text}")
|
||||
elif parameter == "E1":
|
||||
self._send(f"echo:{text}")
|
||||
else:
|
||||
self._send(text)
|
||||
return True
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def _gcode_M400(self, data: str) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _check_param_letters(letters, data):
|
||||
# Checks if any of the params (letters) are included in data
|
||||
# Purely for saving typing :)
|
||||
for param in list(letters):
|
||||
if param in data:
|
||||
return True
|
||||
|
||||
##~~ further helpers
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def _calculate_checksum(self, line: bytes) -> int:
|
||||
checksum = 0
|
||||
for c in bytearray(line):
|
||||
checksum ^= c
|
||||
return checksum
|
||||
|
||||
def _kill(self):
|
||||
self._killed = True
|
||||
if self.bambu.connected:
|
||||
self.bambu.disconnect()
|
||||
self._send("echo:EMERGENCY SHUTDOWN DETECTED. KILLED.")
|
||||
|
||||
def _triggerResend(
|
||||
self, expected: int = None, actual: int = None, checksum: int = None
|
||||
) -> None:
|
||||
with self._incoming_lock:
|
||||
if expected is None:
|
||||
expected = self.lastN + 1
|
||||
else:
|
||||
self.lastN = expected - 1
|
||||
|
||||
if actual is None:
|
||||
if checksum:
|
||||
self._send(self._error("checksum_mismatch"))
|
||||
else:
|
||||
self._send(self._error("checksum_missing"))
|
||||
else:
|
||||
self._send(self._error("lineno_mismatch", expected, actual))
|
||||
|
||||
def request_resend():
|
||||
self._send("Resend:%d" % expected)
|
||||
# if not self._brokenResend:
|
||||
self._sendOk()
|
||||
|
||||
request_resend()
|
||||
|
||||
def _listSd(self, incl_long=False, incl_timestamp=False):
|
||||
line = "{dosname} {size} {timestamp} \"{name}\""
|
||||
|
||||
self._send("Begin file list")
|
||||
for item in map(lambda x: line.format(**x), self._getSdFiles()):
|
||||
self._send(item)
|
||||
self._send("End file list")
|
||||
|
||||
def _mappedSdList(self) -> Dict[str, Dict[str, Any]]:
|
||||
result = {}
|
||||
host = self._settings.get(["host"])
|
||||
access_code = self._settings.get(["access_code"])
|
||||
|
||||
ftp = IoTFTPSClient(f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True)
|
||||
filelist = ftp.list_files("", ".3mf")
|
||||
|
||||
for entry in filelist:
|
||||
if entry.startswith("/"):
|
||||
filename = entry[1:]
|
||||
else:
|
||||
filename = entry
|
||||
filesize = ftp.ftps_session.size(entry)
|
||||
date_str = ftp.ftps_session.sendcmd(f"MDTM {entry}").replace("213 ", "")
|
||||
filedate = datetime.datetime.strptime(date_str, "%Y%m%d%H%M%S").replace(tzinfo=datetime.timezone.utc).timestamp()
|
||||
dosname = get_dos_filename(filename, existing_filenames=list(result.keys())).lower()
|
||||
data = {
|
||||
"dosname": dosname,
|
||||
"name": filename,
|
||||
"path": filename,
|
||||
"size": filesize,
|
||||
"timestamp": unix_timestamp_to_m20_timestamp(int(filedate))
|
||||
}
|
||||
result[filename.lower()] = data
|
||||
result[dosname.lower()] = filename.lower()
|
||||
|
||||
return result
|
||||
|
||||
def _getSdFileData(self, filename: str) -> Optional[Dict[str, Any]]:
|
||||
files = self._mappedSdList()
|
||||
data = files.get(filename.lower())
|
||||
if isinstance(data, str):
|
||||
data = files.get(data.lower())
|
||||
return data
|
||||
|
||||
def _getSdFiles(self) -> List[Dict[str, Any]]:
|
||||
files = self._mappedSdList()
|
||||
return [x for x in files.values() if isinstance(x, dict)]
|
||||
|
||||
def _selectSdFile(self, filename: str, check_already_open: bool = False) -> None:
|
||||
if filename.startswith("/"):
|
||||
filename = filename[1:]
|
||||
|
||||
file = self._getSdFileData(filename)
|
||||
if file is None:
|
||||
self._send(f"{filename} open failed")
|
||||
return
|
||||
|
||||
if self._selectedSdFile == file["path"] and check_already_open:
|
||||
return
|
||||
|
||||
self._selectedSdFile = file["path"]
|
||||
self._selectedSdFileSize = file["size"]
|
||||
self._send(f"File opened: {file['name']} Size: {self._selectedSdFileSize}")
|
||||
self._send("File selected")
|
||||
|
||||
def _startSdPrint(self, from_printer: bool = False) -> None:
|
||||
if self._selectedSdFile is not None:
|
||||
if self._sdPrinter is None:
|
||||
self._sdPrinting = True
|
||||
self._sdPrinter = threading.Thread(target=self._sdPrintingWorker, kwargs={"from_printer": from_printer})
|
||||
self._sdPrinter.start()
|
||||
# self._sdPrintingSemaphore.set()
|
||||
if self._sdPrinter is not None:
|
||||
if self.bambu.connected:
|
||||
if self.bambu.publish(commands.RESUME):
|
||||
self._logger.info("print resumed")
|
||||
# if not self._sdPrintingSemaphore.is_set():
|
||||
# self._sdPrintingSemaphore.set()
|
||||
else:
|
||||
self._logger.info("print resume failed")
|
||||
|
||||
def _pauseSdPrint(self):
|
||||
if self.bambu.connected:
|
||||
if self.bambu.publish(commands.PAUSE):
|
||||
self._logger.info("print paused")
|
||||
else:
|
||||
self._logger.info("print pause failed")
|
||||
|
||||
def _cancelSdPrint(self):
|
||||
if self.bambu.connected:
|
||||
if self.bambu.publish(commands.STOP):
|
||||
self._logger.info("print cancelled")
|
||||
else:
|
||||
self._logger.info("print cancel failed")
|
||||
|
||||
def _setSdPos(self, pos):
|
||||
self._newSdFilePos = pos
|
||||
|
||||
def _reportSdStatus(self):
|
||||
if self._sdPrinter is not None and (self._sdPrintingSemaphore.is_set() or self._sdPrintingPausedSemaphore.is_set()):
|
||||
self._send(f"SD printing byte {self._selectedSdFilePos}/{self._selectedSdFileSize}")
|
||||
else:
|
||||
self._send("Not SD printing")
|
||||
|
||||
def _generateTemperatureOutput(self) -> str:
|
||||
template = "{heater}:{actual:.2f}/ {target:.2f}"
|
||||
temps = collections.OrderedDict()
|
||||
temps["T"] = (self.temp[0], self.targetTemp[0])
|
||||
temps["B"] = (self.bedTemp, self.bedTargetTemp)
|
||||
if self._hasChamber:
|
||||
temps["C"] = (self.chamberTemp, self.chamberTargetTemp)
|
||||
|
||||
output = " ".join(
|
||||
map(
|
||||
lambda x: template.format(heater=x[0], actual=x[1][0], target=x[1][1]),
|
||||
temps.items(),
|
||||
)
|
||||
)
|
||||
output += " @:64\n"
|
||||
return output
|
||||
|
||||
def _processTemperatureQuery(self):
|
||||
# includeOk = not self._okBeforeCommandOutput
|
||||
output = self._generateTemperatureOutput()
|
||||
self._send(output)
|
||||
|
||||
def _writeSdFile(self, filename: str) -> None:
|
||||
self._send(f"Writing to file: {filename}")
|
||||
|
||||
def _finishSdFile(self):
|
||||
try:
|
||||
self._writingToSdHandle.close()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self._writingToSdHandle = None
|
||||
self._writingToSd = False
|
||||
self._selectedSdFile = None
|
||||
# Most printers don't have RTC and set some ancient date
|
||||
# by default. Emulate that using 2000-01-01 01:00:00
|
||||
# (taken from prusa firmware behaviour)
|
||||
st = os.stat(self._writingToSdFile)
|
||||
os.utime(self._writingToSdFile, (st.st_atime, 946684800))
|
||||
self._writingToSdFile = None
|
||||
self._send("Done saving file")
|
||||
|
||||
def _sdPrintingWorker(self, from_printer: bool = False):
|
||||
self._selectedSdFilePos = 0
|
||||
try:
|
||||
if not from_printer and self.bambu.connected:
|
||||
print_command = {"print": {"sequence_id": 0,
|
||||
"command": "project_file",
|
||||
"param": "Metadata/plate_1.gcode",
|
||||
"subtask_name": f"{self._selectedSdFile}",
|
||||
"url": f"file:///mnt/sdcard/{self._selectedSdFile}" if self._settings.get_boolean(["device_type"]) in ["X1", "X1C"] else f"file:///sdcard/{self._selectedSdFile}",
|
||||
"timelapse": self._settings.get_boolean(["timelapse"]),
|
||||
"bed_leveling": self._settings.get_boolean(["bed_leveling"]),
|
||||
"flow_cali": self._settings.get_boolean(["flow_cali"]),
|
||||
"vibration_cali": self._settings.get_boolean(["vibration_cali"]),
|
||||
"layer_inspect": self._settings.get_boolean(["layer_inspect"]),
|
||||
"use_ams": self._settings.get_boolean(["use_ams"])
|
||||
}
|
||||
}
|
||||
self.bambu.publish(print_command)
|
||||
|
||||
while self._selectedSdFilePos < self._selectedSdFileSize:
|
||||
if self._killed or not self._sdPrinting:
|
||||
break
|
||||
|
||||
# if we are paused, wait for resuming
|
||||
self._sdPrintingSemaphore.wait()
|
||||
self._reportSdStatus()
|
||||
time.sleep(3)
|
||||
self._logger.debug(f"SD File Print: {self._selectedSdFile}")
|
||||
except AttributeError:
|
||||
if self.outgoing is not None:
|
||||
raise
|
||||
|
||||
self._finishSdPrint()
|
||||
|
||||
def _finishSdPrint(self):
|
||||
if not self._killed:
|
||||
self._sdPrintingSemaphore.clear()
|
||||
self._sdPrintingPausedSemaphore.clear()
|
||||
self._send("Done printing file")
|
||||
self._selectedSdFilePos = 0
|
||||
self._selectedSdFileSize = 0
|
||||
self._sdPrinting = False
|
||||
self._sdPrinter = None
|
||||
|
||||
def _deleteSdFile(self, filename: str) -> None:
|
||||
host = self._settings.get(["host"])
|
||||
access_code = self._settings.get(["access_code"])
|
||||
|
||||
if filename.startswith("/"):
|
||||
filename = filename[1:]
|
||||
file = self._getSdFileData(filename)
|
||||
if file is not None:
|
||||
ftp = IoTFTPSClient(f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True)
|
||||
try:
|
||||
if ftp.delete_file(filename):
|
||||
self._logger.debug(f"{filename} deleted")
|
||||
else:
|
||||
raise Exception("delete failed")
|
||||
except Exception as e:
|
||||
self._logger.debug(f"Error deleting file {filename}")
|
||||
|
||||
def _setBusy(self, reason="processing"):
|
||||
if not self._sendBusy:
|
||||
return
|
||||
|
||||
def loop():
|
||||
while self._busy:
|
||||
self._send(f"echo:busy {self._busy}")
|
||||
time.sleep(self._busyInterval)
|
||||
self._sendOk()
|
||||
|
||||
self._busy = reason
|
||||
self._busy_loop = threading.Thread(target=loop)
|
||||
self._busy_loop.daemon = True
|
||||
self._busy_loop.start()
|
||||
|
||||
def _setUnbusy(self):
|
||||
self._busy = None
|
||||
|
||||
# def _processBuffer(self):
|
||||
# while self.buffered is not None:
|
||||
# try:
|
||||
# line = self.buffered.get(timeout=0.5)
|
||||
# except queue.Empty:
|
||||
# continue
|
||||
#
|
||||
# if line is None:
|
||||
# continue
|
||||
#
|
||||
# self.buffered.task_done()
|
||||
#
|
||||
# self._logger.debug("Closing down buffer loop")
|
||||
|
||||
def _showPrompt(self, text, choices):
|
||||
self._hidePrompt()
|
||||
self._send(f"//action:prompt_begin {text}")
|
||||
for choice in choices:
|
||||
self._send(f"//action:prompt_button {choice}")
|
||||
self._send("//action:prompt_show")
|
||||
|
||||
def _hidePrompt(self):
|
||||
self._send("//action:prompt_end")
|
||||
|
||||
def write(self, data: bytes) -> int:
|
||||
data = to_bytes(data, errors="replace")
|
||||
u_data = to_unicode(data, errors="replace")
|
||||
|
||||
with self._incoming_lock:
|
||||
if self.incoming is None or self.outgoing is None:
|
||||
return 0
|
||||
|
||||
if b"M112" in data:
|
||||
self._seriallog.debug(f"<<< {u_data}")
|
||||
self._kill()
|
||||
return len(data)
|
||||
|
||||
try:
|
||||
written = self.incoming.put(data, timeout=self._write_timeout, partial=True)
|
||||
self._seriallog.debug(f"<<< {u_data}")
|
||||
return written
|
||||
except queue.Full:
|
||||
self._logger.info(
|
||||
"Incoming queue is full, raising SerialTimeoutException"
|
||||
)
|
||||
raise SerialTimeoutException()
|
||||
|
||||
def readline(self) -> bytes:
|
||||
timeout = self._read_timeout
|
||||
|
||||
try:
|
||||
# fetch a line from the queue, wait no longer than timeout
|
||||
line = to_unicode(self.outgoing.get(timeout=timeout), errors="replace")
|
||||
self._seriallog.debug(f">>> {line.strip()}")
|
||||
self.outgoing.task_done()
|
||||
return to_bytes(line)
|
||||
except queue.Empty:
|
||||
# queue empty? return empty line
|
||||
return b""
|
||||
|
||||
def close(self):
|
||||
if self.bambu.connected:
|
||||
self.bambu.disconnect()
|
||||
self._killed = True
|
||||
self.incoming = None
|
||||
self.outgoing = None
|
||||
self.buffered = None
|
||||
|
||||
def _sendOk(self):
|
||||
if self.outgoing is None:
|
||||
return
|
||||
ok = self._ok()
|
||||
if ok:
|
||||
self._send(ok)
|
||||
|
||||
def _isPaused(self):
|
||||
return self._sdPrintingPausedSemaphore.is_set()
|
||||
def _sendPaused(self):
|
||||
paused_timer = RepeatedTimer(interval=3.0, function=self._send, args=[f"SD printing byte {self._selectedSdFilePos}/{self._selectedSdFileSize}"],
|
||||
daemon=True, run_first=True, condition=self._isPaused)
|
||||
paused_timer.start()
|
||||
|
||||
def _send(self, line: str) -> None:
|
||||
if self.outgoing is not None:
|
||||
self.outgoing.put(line)
|
||||
|
||||
def _ok(self):
|
||||
return "ok"
|
||||
|
||||
def _error(self, error: str, *args, **kwargs) -> str:
|
||||
return f"Error: {self._errors.get(error).format(*args, **kwargs)}"
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
class CharCountingQueue(queue.Queue):
|
||||
def __init__(self, maxsize, name=None):
|
||||
queue.Queue.__init__(self, maxsize=maxsize)
|
||||
self._size = 0
|
||||
self._name = name
|
||||
|
||||
def clear(self):
|
||||
with self.mutex:
|
||||
self.queue.clear()
|
||||
|
||||
def put(self, item, block=True, timeout=None, partial=False) -> int:
|
||||
self.not_full.acquire()
|
||||
|
||||
try:
|
||||
if not self._will_it_fit(item) and partial:
|
||||
space_left = self.maxsize - self._qsize()
|
||||
if space_left:
|
||||
item = item[:space_left]
|
||||
|
||||
if not block:
|
||||
if not self._will_it_fit(item):
|
||||
raise queue.Full
|
||||
elif timeout is None:
|
||||
while not self._will_it_fit(item):
|
||||
self.not_full.wait()
|
||||
elif timeout < 0:
|
||||
raise ValueError("'timeout' must be a positive number")
|
||||
else:
|
||||
endtime = time.monotonic() + timeout
|
||||
while not self._will_it_fit(item):
|
||||
remaining = endtime - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise queue.Full
|
||||
self.not_full.wait(remaining)
|
||||
|
||||
self._put(item)
|
||||
self.unfinished_tasks += 1
|
||||
self.not_empty.notify()
|
||||
|
||||
return self._len(item)
|
||||
finally:
|
||||
self.not_full.release()
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def _len(self, item):
|
||||
return len(item)
|
||||
|
||||
def _qsize(self, l=len): # noqa: E741
|
||||
return self._size
|
||||
|
||||
# Put a new item in the queue
|
||||
def _put(self, item):
|
||||
self.queue.append(item)
|
||||
self._size += self._len(item)
|
||||
|
||||
# Get an item from the queue
|
||||
def _get(self):
|
||||
item = self.queue.popleft()
|
||||
self._size -= self._len(item)
|
||||
return item
|
||||
|
||||
def _will_it_fit(self, item):
|
||||
return self.maxsize - self._qsize() >= self._len(item)
|
@ -7,3 +7,10 @@
|
||||
###
|
||||
|
||||
.
|
||||
|
||||
pytest~=7.4.4
|
||||
pybambu~=1.0.1
|
||||
OctoPrint~=1.10.2
|
||||
setuptools~=70.0.0
|
||||
pyserial~=3.5
|
||||
Flask~=2.2.5
|
||||
|
6
setup.py
6
setup.py
@ -14,7 +14,7 @@ plugin_package = "octoprint_bambu_printer"
|
||||
plugin_name = "OctoPrint-BambuPrinter"
|
||||
|
||||
# The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module
|
||||
plugin_version = "0.0.6"
|
||||
plugin_version = "0.1.8rc14"
|
||||
|
||||
# The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin
|
||||
# module
|
||||
@ -33,7 +33,7 @@ plugin_url = "https://github.com/jneilliii/OctoPrint-BambuPrinter"
|
||||
plugin_license = "AGPLv3"
|
||||
|
||||
# Any additional requirements besides OctoPrint should be listed here
|
||||
plugin_requires = ["paho-mqtt", "python-dateutil", "pybambu>=1.0.1"]
|
||||
plugin_requires = ["paho-mqtt<2", "python-dateutil", "cloudscraper"]
|
||||
|
||||
### --------------------------------------------------------------------------------------------------------------------
|
||||
### More advanced options that you usually shouldn't have to touch follow after this point
|
||||
@ -43,7 +43,7 @@ plugin_requires = ["paho-mqtt", "python-dateutil", "pybambu>=1.0.1"]
|
||||
# already be installed automatically if they exist. Note that if you add something here you'll also need to update
|
||||
# MANIFEST.in to match to ensure that python setup.py sdist produces a source distribution that contains all your
|
||||
# files. This is sadly due to how python's setup.py works, see also http://stackoverflow.com/a/14159430/2028598
|
||||
plugin_additional_data = []
|
||||
plugin_additional_data = ["octoprint_bambu_printer/printer/pybambu/filaments.json"]
|
||||
|
||||
# Any additional python packages you need to install with your plugin that are not contained in <plugin_package>.*
|
||||
plugin_additional_packages = []
|
||||
|
0
test/__init__.py
Normal file
0
test/__init__.py
Normal file
9
test/conftest.py
Normal file
9
test/conftest.py
Normal file
@ -0,0 +1,9 @@
|
||||
from pathlib import Path
|
||||
from pytest import fixture
|
||||
|
||||
|
||||
@fixture
|
||||
def output_folder():
|
||||
folder = Path(__file__).parent / "test_output"
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
return folder
|
30
test/test_data_conversions.py
Normal file
30
test/test_data_conversions.py
Normal file
@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from octoprint.util import get_formatted_size, get_formatted_datetime
|
||||
from octoprint_bambu_printer.printer.file_system.bambu_timelapse_file_info import (
|
||||
BambuTimelapseFileInfo,
|
||||
)
|
||||
from octoprint_bambu_printer.printer.file_system.file_info import FileInfo
|
||||
|
||||
|
||||
def test_timelapse_info_valid():
|
||||
file_name = "part.mp4"
|
||||
file_size = 1000
|
||||
file_date = datetime(2020, 1, 1)
|
||||
file_timestamp = file_date.timestamp()
|
||||
|
||||
file_info = FileInfo(file_name, Path(file_name), file_size, file_date)
|
||||
timelapse = BambuTimelapseFileInfo.from_file_info(file_info)
|
||||
|
||||
assert timelapse.to_dict() == {
|
||||
"bytes": file_size,
|
||||
"date": get_formatted_datetime(datetime.fromtimestamp(file_timestamp)),
|
||||
"name": file_name,
|
||||
"size": get_formatted_size(file_size),
|
||||
"thumbnail": "/plugin/bambu_printer/thumbnail/"
|
||||
+ file_name.replace(".mp4", ".jpg").replace(".avi", ".jpg"),
|
||||
"timestamp": file_timestamp,
|
||||
"url": f"/plugin/bambu_printer/timelapse/{file_name}",
|
||||
}
|
562
test/test_gcode_execution.py
Normal file
562
test/test_gcode_execution.py
Normal file
@ -0,0 +1,562 @@
|
||||
from __future__ import annotations
|
||||
from datetime import datetime, timezone
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView
|
||||
import octoprint_bambu_printer.printer.pybambu
|
||||
import octoprint_bambu_printer.printer.pybambu.commands
|
||||
from octoprint_bambu_printer.printer.bambu_virtual_printer import BambuVirtualPrinter
|
||||
from octoprint_bambu_printer.printer.file_system.file_info import FileInfo
|
||||
from octoprint_bambu_printer.printer.file_system.ftps_client import IoTFTPSClient
|
||||
from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import (
|
||||
RemoteSDCardFileList,
|
||||
)
|
||||
from octoprint_bambu_printer.printer.states.idle_state import IdleState
|
||||
from octoprint_bambu_printer.printer.states.paused_state import PausedState
|
||||
from octoprint_bambu_printer.printer.states.printing_state import PrintingState
|
||||
from pytest import fixture
|
||||
|
||||
|
||||
@fixture
|
||||
def output_test_folder(output_folder: Path):
|
||||
folder = output_folder / "test_gcode"
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
return folder
|
||||
|
||||
|
||||
@fixture
|
||||
def log_test():
|
||||
log = logging.getLogger("gcode_unittest")
|
||||
log.setLevel(logging.DEBUG)
|
||||
return log
|
||||
|
||||
|
||||
class DictGetter:
|
||||
def __init__(self, options: dict, default_value=None) -> None:
|
||||
self.options: dict[str | tuple[str, ...], Any] = options
|
||||
self._default_value = default_value
|
||||
|
||||
def __call__(self, key: str | list[str] | tuple[str, ...]):
|
||||
if isinstance(key, list):
|
||||
key = tuple(key)
|
||||
return self.options.get(key, self._default_value)
|
||||
|
||||
|
||||
@fixture
|
||||
def settings(output_test_folder):
|
||||
_settings = MagicMock()
|
||||
_settings.get.side_effect = DictGetter(
|
||||
{
|
||||
"serial": "BAMBU",
|
||||
"host": "localhost",
|
||||
"access_code": "12345",
|
||||
}
|
||||
)
|
||||
_settings.get_boolean.side_effect = DictGetter({"forceChecksum": False})
|
||||
|
||||
log_file_path = output_test_folder / "log.txt"
|
||||
log_file_path.touch()
|
||||
_settings.get_plugin_logfile_path.return_value = log_file_path.as_posix()
|
||||
return _settings
|
||||
|
||||
|
||||
@fixture
|
||||
def profile_manager():
|
||||
_profile_manager = MagicMock()
|
||||
_profile_manager.get_current.side_effect = MagicMock()
|
||||
_profile_manager.get_current().get.side_effect = DictGetter(
|
||||
{
|
||||
"heatedChamber": False,
|
||||
}
|
||||
)
|
||||
return _profile_manager
|
||||
|
||||
|
||||
def _ftp_date_format(dt: datetime):
|
||||
return dt.replace(tzinfo=timezone.utc).strftime("%Y%m%d%H%M%S")
|
||||
|
||||
|
||||
@fixture
|
||||
def project_files_info_ftp():
|
||||
return {
|
||||
"print.3mf": (1000, _ftp_date_format(datetime(2024, 5, 6))),
|
||||
"print2.3mf": (1200, _ftp_date_format(datetime(2024, 5, 7))),
|
||||
}
|
||||
|
||||
|
||||
@fixture
|
||||
def cache_files_info_ftp():
|
||||
return {
|
||||
"cache/print.3mf": (1200, _ftp_date_format(datetime(2024, 5, 7))),
|
||||
"cache/print3.gcode.3mf": (1200, _ftp_date_format(datetime(2024, 5, 7))),
|
||||
"cache/long file path with spaces.gcode.3mf": (
|
||||
1200,
|
||||
_ftp_date_format(datetime(2024, 5, 7)),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@fixture
|
||||
def ftps_session_mock(project_files_info_ftp, cache_files_info_ftp):
|
||||
all_file_info = dict(**project_files_info_ftp, **cache_files_info_ftp)
|
||||
ftps_session = MagicMock()
|
||||
ftps_session.size.side_effect = DictGetter(
|
||||
{file: info[0] for file, info in all_file_info.items()}
|
||||
)
|
||||
|
||||
ftps_session.sendcmd.side_effect = DictGetter(
|
||||
{f"MDTM {file}": info[1] for file, info in all_file_info.items()}
|
||||
)
|
||||
|
||||
ftps_session.nlst.side_effect = DictGetter(
|
||||
{
|
||||
"": list(map(lambda p: Path(p).name, project_files_info_ftp))
|
||||
+ ["Mock folder"],
|
||||
"cache/": list(map(lambda p: Path(p).name, cache_files_info_ftp))
|
||||
+ ["Mock folder"],
|
||||
"timelapse/": ["video.mp4", "video.avi"],
|
||||
}
|
||||
)
|
||||
IoTFTPSClient.open_ftps_session = MagicMock(return_value=ftps_session)
|
||||
yield ftps_session
|
||||
|
||||
|
||||
@fixture(scope="function")
|
||||
def print_job_mock():
|
||||
print_job = MagicMock()
|
||||
print_job.subtask_name = ""
|
||||
print_job.print_percentage = 0
|
||||
return print_job
|
||||
|
||||
|
||||
@fixture(scope="function")
|
||||
def temperatures_mock():
|
||||
temperatures = MagicMock()
|
||||
temperatures.nozzle_temp = 0
|
||||
temperatures.target_nozzle_temp = 0
|
||||
temperatures.bed_temp = 0
|
||||
temperatures.target_bed_temp = 0
|
||||
temperatures.chamber_temp = 0
|
||||
return temperatures
|
||||
|
||||
|
||||
@fixture(scope="function")
|
||||
def bambu_client_mock(print_job_mock, temperatures_mock) -> pybambu.BambuClient:
|
||||
bambu_client = MagicMock()
|
||||
bambu_client.connected = True
|
||||
device_mock = MagicMock()
|
||||
device_mock.print_job = print_job_mock
|
||||
device_mock.temperatures = temperatures_mock
|
||||
bambu_client.get_device.return_value = device_mock
|
||||
return bambu_client
|
||||
|
||||
|
||||
@fixture(scope="function")
|
||||
def printer(
|
||||
output_test_folder,
|
||||
settings,
|
||||
profile_manager,
|
||||
log_test,
|
||||
ftps_session_mock,
|
||||
bambu_client_mock,
|
||||
):
|
||||
async def _mock_connection(self):
|
||||
pass
|
||||
|
||||
BambuVirtualPrinter._create_client_connection_async = _mock_connection
|
||||
printer_test = BambuVirtualPrinter(
|
||||
settings,
|
||||
profile_manager,
|
||||
data_folder=output_test_folder,
|
||||
serial_log_handler=log_test,
|
||||
read_timeout=0.01,
|
||||
faked_baudrate=115200,
|
||||
)
|
||||
printer_test._bambu_client = bambu_client_mock
|
||||
printer_test.flush()
|
||||
printer_test.readlines()
|
||||
yield printer_test
|
||||
printer_test.close()
|
||||
|
||||
|
||||
def test_initial_state(printer: BambuVirtualPrinter):
|
||||
assert isinstance(printer.current_state, IdleState)
|
||||
|
||||
|
||||
def test_list_sd_card(printer: BambuVirtualPrinter):
|
||||
printer.write(b"M20\n") # GCode for listing SD card
|
||||
printer.flush()
|
||||
result = printer.readlines()
|
||||
assert result[0] == b"Begin file list"
|
||||
assert result[1].endswith(b'"print.3mf"')
|
||||
assert result[2].endswith(b'"print2.3mf"')
|
||||
assert result[3].endswith(b'"print.3mf"')
|
||||
assert result[4].endswith(b'"print3.gcode.3mf"')
|
||||
assert result[-3] == b"End file list"
|
||||
assert result[-2] == b"ok"
|
||||
assert result[-1] == b"ok"
|
||||
|
||||
|
||||
def test_list_ftp_paths_p1s(settings, ftps_session_mock):
|
||||
file_system = RemoteSDCardFileList(settings)
|
||||
file_view = CachedFileView(file_system).with_filter("timelapse/", ".avi")
|
||||
|
||||
timelapse_files = ["timelapse/video.avi", "timelapse/video2.avi"]
|
||||
ftps_session_mock.size.side_effect = DictGetter(
|
||||
{file: 100 for file in timelapse_files}
|
||||
)
|
||||
ftps_session_mock.sendcmd.side_effect = DictGetter(
|
||||
{
|
||||
f"MDTM {file}": _ftp_date_format(datetime(2024, 5, 7))
|
||||
for file in timelapse_files
|
||||
}
|
||||
)
|
||||
ftps_session_mock.nlst.side_effect = DictGetter(
|
||||
{"timelapse/": [Path(f).name for f in timelapse_files]}
|
||||
)
|
||||
|
||||
timelapse_paths = list(map(Path, timelapse_files))
|
||||
result_files = file_view.get_all_info()
|
||||
assert len(timelapse_files) == len(result_files) and all(
|
||||
file_info.path in timelapse_paths for file_info in result_files
|
||||
)
|
||||
|
||||
|
||||
def test_list_ftp_paths_x1(settings, ftps_session_mock):
|
||||
file_system = RemoteSDCardFileList(settings)
|
||||
file_view = CachedFileView(file_system).with_filter("timelapse/", ".mp4")
|
||||
|
||||
timelapse_files = ["timelapse/video.mp4", "timelapse/video2.mp4"]
|
||||
ftps_session_mock.size.side_effect = DictGetter(
|
||||
{file: 100 for file in timelapse_files}
|
||||
)
|
||||
ftps_session_mock.sendcmd.side_effect = DictGetter(
|
||||
{
|
||||
f"MDTM {file}": _ftp_date_format(datetime(2024, 5, 7))
|
||||
for file in timelapse_files
|
||||
}
|
||||
)
|
||||
ftps_session_mock.nlst.side_effect = DictGetter({"timelapse/": timelapse_files})
|
||||
|
||||
timelapse_paths = list(map(Path, timelapse_files))
|
||||
result_files = file_view.get_all_info()
|
||||
assert len(timelapse_files) == len(result_files) and all(
|
||||
file_info.path in timelapse_paths for file_info in result_files
|
||||
)
|
||||
|
||||
|
||||
def test_delete_sd_file_gcode(printer: BambuVirtualPrinter):
|
||||
with patch(
|
||||
"octoprint_bambu_printer.printer.file_system.ftps_client.IoTFTPSConnection.delete_file"
|
||||
) as delete_function:
|
||||
printer.write(b"M30 print.3mf\n")
|
||||
printer.flush()
|
||||
result = printer.readlines()
|
||||
assert result[-1] == b"ok"
|
||||
delete_function.assert_called_with("print.3mf")
|
||||
|
||||
printer.write(b"M30 cache/print.3mf\n")
|
||||
printer.flush()
|
||||
result = printer.readlines()
|
||||
assert result[-1] == b"ok"
|
||||
delete_function.assert_called_with("cache/print.3mf")
|
||||
|
||||
|
||||
def test_delete_sd_file_by_dosname(printer: BambuVirtualPrinter):
|
||||
with patch(
|
||||
"octoprint_bambu_printer.printer.file_system.ftps_client.IoTFTPSConnection.delete_file"
|
||||
) as delete_function:
|
||||
file_info = printer.project_files.get_file_data("cache/print.3mf")
|
||||
assert file_info is not None
|
||||
|
||||
printer.write(b"M30 " + file_info.dosname.encode() + b"\n")
|
||||
printer.flush()
|
||||
assert printer.readlines()[-1] == b"ok"
|
||||
assert delete_function.call_count == 1
|
||||
delete_function.assert_called_with("cache/print.3mf")
|
||||
|
||||
printer.write(b"M30 cache/print.3mf\n")
|
||||
printer.flush()
|
||||
assert printer.readlines()[-1] == b"ok"
|
||||
assert delete_function.call_count == 2
|
||||
delete_function.assert_called_with("cache/print.3mf")
|
||||
|
||||
|
||||
def test_select_project_file_by_stem(printer: BambuVirtualPrinter):
|
||||
printer.write(b"M23 print3\n")
|
||||
printer.flush()
|
||||
result = printer.readlines()
|
||||
assert printer.selected_file is not None
|
||||
assert printer.selected_file.path == Path("cache/print3.gcode.3mf")
|
||||
assert result[-2] == b"File selected"
|
||||
assert result[-1] == b"ok"
|
||||
|
||||
|
||||
def test_select_project_long_name_file_with_multiple_extensions(
|
||||
printer: BambuVirtualPrinter,
|
||||
):
|
||||
printer.write(b"M23 long file path with spaces.gcode.3mf\n")
|
||||
printer.flush()
|
||||
result = printer.readlines()
|
||||
assert printer.selected_file is not None
|
||||
assert printer.selected_file.path == Path(
|
||||
"cache/long file path with spaces.gcode.3mf"
|
||||
)
|
||||
assert result[-2] == b"File selected"
|
||||
assert result[-1] == b"ok"
|
||||
|
||||
|
||||
def test_cannot_start_print_without_file(printer: BambuVirtualPrinter):
|
||||
printer.write(b"M24\n")
|
||||
printer.flush()
|
||||
result = printer.readlines()
|
||||
assert result[0] == b"ok"
|
||||
assert isinstance(printer.current_state, IdleState)
|
||||
|
||||
|
||||
def test_non_existing_file_not_selected(printer: BambuVirtualPrinter):
|
||||
assert printer.selected_file is None
|
||||
|
||||
printer.write(b"M23 non_existing.3mf\n")
|
||||
printer.flush()
|
||||
result = printer.readlines()
|
||||
assert result[-2] != b"File selected"
|
||||
assert result[-1] == b"ok"
|
||||
assert printer.selected_file is None
|
||||
|
||||
|
||||
def test_print_started_with_selected_file(printer: BambuVirtualPrinter, print_job_mock):
|
||||
assert printer.selected_file is None
|
||||
|
||||
printer.write(b"M20\n")
|
||||
printer.flush()
|
||||
printer.readlines()
|
||||
|
||||
printer.write(b"M23 print.3mf\n")
|
||||
printer.flush()
|
||||
result = printer.readlines()
|
||||
assert result[-2] == b"File selected"
|
||||
assert result[-1] == b"ok"
|
||||
|
||||
assert printer.selected_file is not None
|
||||
assert printer.selected_file.file_name == "print.3mf"
|
||||
|
||||
print_job_mock.subtask_name = "print.3mf"
|
||||
|
||||
printer.write(b"M24\n")
|
||||
printer.flush()
|
||||
result = printer.readlines()
|
||||
assert result[-1] == b"ok"
|
||||
|
||||
# emulate printer reporting it's status
|
||||
print_job_mock.gcode_state = "RUNNING"
|
||||
printer.new_update("event_printer_data_update")
|
||||
printer.flush()
|
||||
assert isinstance(printer.current_state, PrintingState)
|
||||
|
||||
|
||||
def test_pause_print(printer: BambuVirtualPrinter, bambu_client_mock, print_job_mock):
|
||||
print_job_mock.subtask_name = "print.3mf"
|
||||
|
||||
printer.write(b"M20\n")
|
||||
printer.write(b"M23 print.3mf\n")
|
||||
printer.write(b"M24\n")
|
||||
printer.flush()
|
||||
|
||||
print_job_mock.gcode_state = "RUNNING"
|
||||
printer.new_update("event_printer_data_update")
|
||||
printer.flush()
|
||||
assert isinstance(printer.current_state, PrintingState)
|
||||
|
||||
printer.write(b"M25\n") # pausing the print
|
||||
printer.flush()
|
||||
result = printer.readlines()
|
||||
assert result[-1] == b"ok"
|
||||
|
||||
print_job_mock.gcode_state = "PAUSE"
|
||||
printer.new_update("event_printer_data_update")
|
||||
printer.flush()
|
||||
assert isinstance(printer.current_state, PausedState)
|
||||
bambu_client_mock.publish.assert_called_with(pybambu.commands.PAUSE)
|
||||
|
||||
|
||||
def test_events_update_printer_state(printer: BambuVirtualPrinter, print_job_mock):
|
||||
print_job_mock.subtask_name = "print.3mf"
|
||||
print_job_mock.gcode_state = "RUNNING"
|
||||
printer.new_update("event_printer_data_update")
|
||||
printer.flush()
|
||||
assert isinstance(printer.current_state, PrintingState)
|
||||
|
||||
print_job_mock.gcode_state = "PAUSE"
|
||||
printer.new_update("event_printer_data_update")
|
||||
printer.flush()
|
||||
assert isinstance(printer.current_state, PausedState)
|
||||
|
||||
print_job_mock.gcode_state = "IDLE"
|
||||
printer.new_update("event_printer_data_update")
|
||||
printer.flush()
|
||||
assert isinstance(printer.current_state, IdleState)
|
||||
|
||||
print_job_mock.gcode_state = "FINISH"
|
||||
printer.new_update("event_printer_data_update")
|
||||
printer.flush()
|
||||
assert isinstance(printer.current_state, IdleState)
|
||||
|
||||
print_job_mock.gcode_state = "FAILED"
|
||||
printer.new_update("event_printer_data_update")
|
||||
printer.flush()
|
||||
assert isinstance(printer.current_state, IdleState)
|
||||
|
||||
|
||||
def test_printer_info_check(printer: BambuVirtualPrinter):
|
||||
printer.write(b"M27\n") # printer get info
|
||||
printer.flush()
|
||||
|
||||
result = printer.readlines()
|
||||
assert result[-1] == b"ok"
|
||||
assert isinstance(printer.current_state, IdleState)
|
||||
|
||||
|
||||
def test_abort_print_during_printing(printer: BambuVirtualPrinter, print_job_mock):
|
||||
print_job_mock.subtask_name = "print.3mf"
|
||||
|
||||
printer.write(b"M20\nM23 print.3mf\nM24\n")
|
||||
printer.flush()
|
||||
print_job_mock.gcode_state = "RUNNING"
|
||||
print_job_mock.print_percentage = 50
|
||||
printer.new_update("event_printer_data_update")
|
||||
printer.flush()
|
||||
printer.readlines()
|
||||
assert isinstance(printer.current_state, PrintingState)
|
||||
|
||||
printer.write(b"M26 S0\n")
|
||||
printer.flush()
|
||||
result = printer.readlines()
|
||||
assert result[-1] == b"ok"
|
||||
assert isinstance(printer.current_state, IdleState)
|
||||
|
||||
|
||||
def test_abort_print_during_pause(printer: BambuVirtualPrinter, print_job_mock):
|
||||
print_job_mock.subtask_name = "print.3mf"
|
||||
|
||||
printer.write(b"M20\nM23 print.3mf\nM24\n")
|
||||
printer.flush()
|
||||
print_job_mock.gcode_state = "RUNNING"
|
||||
printer.new_update("event_printer_data_update")
|
||||
printer.flush()
|
||||
|
||||
printer.write(b"M25\n")
|
||||
printer.flush()
|
||||
print_job_mock.gcode_state = "PAUSE"
|
||||
printer.new_update("event_printer_data_update")
|
||||
printer.flush()
|
||||
|
||||
printer.readlines()
|
||||
assert isinstance(printer.current_state, PausedState)
|
||||
|
||||
printer.write(b"M26 S0\n")
|
||||
printer.flush()
|
||||
result = printer.readlines()
|
||||
assert result[-1] == b"ok"
|
||||
assert isinstance(printer.current_state, IdleState)
|
||||
|
||||
|
||||
def test_regular_move(printer: BambuVirtualPrinter, bambu_client_mock):
|
||||
gcode = b"G28\nG1 X10 Y10\n"
|
||||
printer.write(gcode)
|
||||
printer.flush()
|
||||
result = printer.readlines()
|
||||
assert result[-1] == b"ok"
|
||||
|
||||
gcode_command = pybambu.commands.SEND_GCODE_TEMPLATE
|
||||
gcode_command["print"]["param"] = "G28\n"
|
||||
bambu_client_mock.publish.assert_called_with(gcode_command)
|
||||
|
||||
gcode_command["print"]["param"] = "G1 X10 Y10\n"
|
||||
bambu_client_mock.publish.assert_called_with(gcode_command)
|
||||
|
||||
|
||||
def test_file_selection_does_not_affect_current_print(
|
||||
printer: BambuVirtualPrinter, print_job_mock
|
||||
):
|
||||
print_job_mock.subtask_name = "print.3mf"
|
||||
|
||||
printer.write(b"M23 print.3mf\nM24\n")
|
||||
printer.flush()
|
||||
print_job_mock.gcode_state = "RUNNING"
|
||||
printer.new_update("event_printer_data_update")
|
||||
printer.flush()
|
||||
assert isinstance(printer.current_state, PrintingState)
|
||||
assert printer.current_print_job is not None
|
||||
assert printer.current_print_job.file_info.file_name == "print.3mf"
|
||||
assert printer.current_print_job.progress == 0
|
||||
|
||||
printer.write(b"M23 print2.3mf\n")
|
||||
printer.flush()
|
||||
assert printer.current_print_job is not None
|
||||
assert printer.current_print_job.file_info.file_name == "print.3mf"
|
||||
assert printer.current_print_job.progress == 0
|
||||
|
||||
|
||||
def test_finished_print_job_reset_after_new_file_selected(
|
||||
printer: BambuVirtualPrinter, print_job_mock
|
||||
):
|
||||
print_job_mock.subtask_name = "print.3mf"
|
||||
|
||||
printer.write(b"M23 print.3mf\nM24\n")
|
||||
printer.flush()
|
||||
print_job_mock.gcode_state = "RUNNING"
|
||||
printer.new_update("event_printer_data_update")
|
||||
printer.flush()
|
||||
assert isinstance(printer.current_state, PrintingState)
|
||||
assert printer.current_print_job is not None
|
||||
assert printer.current_print_job.file_info.file_name == "print.3mf"
|
||||
assert printer.current_print_job.progress == 0
|
||||
|
||||
print_job_mock.print_percentage = 100
|
||||
printer.current_state.update_print_job_info()
|
||||
assert isinstance(printer.current_state, PrintingState)
|
||||
assert printer.current_print_job.progress == 100
|
||||
|
||||
print_job_mock.gcode_state = "FINISH"
|
||||
printer.new_update("event_printer_data_update")
|
||||
printer.flush()
|
||||
assert isinstance(printer.current_state, IdleState)
|
||||
assert printer.current_print_job is None
|
||||
assert printer.selected_file is not None
|
||||
assert printer.selected_file.file_name == "print.3mf"
|
||||
|
||||
printer.write(b"M23 print2.3mf\n")
|
||||
printer.flush()
|
||||
assert printer.current_print_job is None
|
||||
assert printer.selected_file is not None
|
||||
assert printer.selected_file.file_name == "print2.3mf"
|
||||
|
||||
|
||||
def test_finish_detected_correctly(printer: BambuVirtualPrinter, print_job_mock):
|
||||
print_job_mock.subtask_name = "print.3mf"
|
||||
print_job_mock.gcode_state = "RUNNING"
|
||||
print_job_mock.print_percentage = 99
|
||||
printer.new_update("event_printer_data_update")
|
||||
printer.flush()
|
||||
assert isinstance(printer.current_state, PrintingState)
|
||||
assert printer.current_print_job is not None
|
||||
assert printer.current_print_job.file_info.file_name == "print.3mf"
|
||||
assert printer.current_print_job.progress == 99
|
||||
|
||||
print_job_mock.print_percentage = 100
|
||||
print_job_mock.gcode_state = "FINISH"
|
||||
printer.new_update("event_printer_data_update")
|
||||
printer.flush()
|
||||
result = printer.readlines()
|
||||
assert result[-3].endswith(b"1000/1000")
|
||||
assert result[-2] == b"Done printing file"
|
||||
assert result[-1] == b"Not SD printing"
|
||||
assert isinstance(printer.current_state, IdleState)
|
||||
assert printer.current_print_job is None
|
||||
assert printer.selected_file is not None
|
||||
assert printer.selected_file.file_name == "print.3mf"
|
Reference in New Issue
Block a user