bambu-printer-manager 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bambu_printer_manager-0.0.1/.DS_Store +0 -0
- bambu_printer_manager-0.0.1/.gitignore +13 -0
- bambu_printer_manager-0.0.1/LICENSE +13 -0
- bambu_printer_manager-0.0.1/PKG-INFO +24 -0
- bambu_printer_manager-0.0.1/README.md +8 -0
- bambu_printer_manager-0.0.1/pyproject.toml +29 -0
- bambu_printer_manager-0.0.1/src/bambu-printer-manager/__init__.py +0 -0
- bambu_printer_manager-0.0.1/src/bambu-printer-manager/bambucommands.py +127 -0
- bambu_printer_manager-0.0.1/src/bambu-printer-manager/bambuconfig.py +109 -0
- bambu_printer_manager-0.0.1/src/bambu-printer-manager/bambulogger.py +78 -0
- bambu_printer_manager-0.0.1/src/bambu-printer-manager/bambuprinter.py +518 -0
- bambu_printer_manager-0.0.1/src/bambu-printer-manager/bambuprinterlogger.json +48 -0
- bambu_printer_manager-0.0.1/src/bambu-printer-manager/bambuspool.py +47 -0
- bambu_printer_manager-0.0.1/src/bambu-printer-manager/bambutools.py +64 -0
|
Binary file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
.yarn/*
|
|
2
|
+
!.yarn/patches
|
|
3
|
+
!.yarn/plugins
|
|
4
|
+
!.yarn/releases
|
|
5
|
+
!.yarn/sdks
|
|
6
|
+
!.yarn/versions
|
|
7
|
+
|
|
8
|
+
# Swap the comments on the following lines if you wish to use zero-installs
|
|
9
|
+
# In that case, don't forget to run `yarn config set enableGlobalCache false`!
|
|
10
|
+
# Documentation here: https://yarnpkg.com/features/caching#zero-installs
|
|
11
|
+
|
|
12
|
+
#!.yarn/cache
|
|
13
|
+
.pnp.*
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
2
|
+
Version 2, December 2004
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
|
5
|
+
|
|
6
|
+
Everyone is permitted to copy and distribute verbatim or modified
|
|
7
|
+
copies of this license document, and changing it is allowed as long
|
|
8
|
+
as the name is changed.
|
|
9
|
+
|
|
10
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
11
|
+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
12
|
+
|
|
13
|
+
0. You just DO WHAT THE FUCK YOU WANT TO.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: bambu-printer-manager
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A pure python library for managing Bambu Labs printers
|
|
5
|
+
Project-URL: Homepage, https://github.com/synman/bambu-printer-manager
|
|
6
|
+
Project-URL: Issues, https://github.com/pypa/bambu-printer-manager/issues
|
|
7
|
+
Author-email: "Shell M. Shrader" <shell@shellware.com>
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Requires-Dist: paho-mqtt
|
|
14
|
+
Requires-Dist: webcolors
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# bambu-printer-manager
|
|
18
|
+
|
|
19
|
+
## <B>Now supports Bambu printers</B>
|
|
20
|
+
|
|
21
|
+
## Dependencies
|
|
22
|
+
```
|
|
23
|
+
pip install paho.mqtt
|
|
24
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bambu-printer-manager"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Shell M. Shrader", email="shell@shellware.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "A pure python library for managing Bambu Labs printers"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"webcolors",
|
|
21
|
+
"paho-mqtt",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["src/"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/synman/bambu-printer-manager"
|
|
29
|
+
Issues = "https://github.com/pypa/bambu-printer-manager/issues"
|
|
File without changes
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
ANNOUNCE_PUSH = {
|
|
2
|
+
"pushing":{
|
|
3
|
+
"command":"pushall",
|
|
4
|
+
"push_target":1,
|
|
5
|
+
"sequence_id":"0",
|
|
6
|
+
"version":1
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
ANNOUNCE_VERSION = {
|
|
11
|
+
"info":{
|
|
12
|
+
"command":"get_version",
|
|
13
|
+
"sequence_id":"0"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
CHAMBER_LIGHT_TOGGLE = {
|
|
18
|
+
"system": {
|
|
19
|
+
"sequence_id": "0",
|
|
20
|
+
"command": "ledctrl",
|
|
21
|
+
"led_node": "chamber_light",
|
|
22
|
+
"led_mode": "on",
|
|
23
|
+
"led_on_time": 500,
|
|
24
|
+
"led_off_time": 500,
|
|
25
|
+
"loop_times": 0,
|
|
26
|
+
"interval_time": 0
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
SPEED_PROFILE_TEMPLATE = {
|
|
31
|
+
"print": {
|
|
32
|
+
"sequence_id": "0",
|
|
33
|
+
"command": "print_speed",
|
|
34
|
+
"param": "2"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
PAUSE_PRINT = {
|
|
39
|
+
"print": {
|
|
40
|
+
"sequence_id": "0",
|
|
41
|
+
"command": "pause"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
RESUME_PRINT = {
|
|
46
|
+
"print": {
|
|
47
|
+
"sequence_id": "0",
|
|
48
|
+
"command": "resume"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
STOP_PRINT = {
|
|
53
|
+
"print": {
|
|
54
|
+
"sequence_id": "0",
|
|
55
|
+
"command": "stop"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
SEND_GCODE_TEMPLATE = {
|
|
60
|
+
"print": {
|
|
61
|
+
"sequence_id": "0",
|
|
62
|
+
"command": "gcode_line",
|
|
63
|
+
"param": ""
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
MOVE_RIGHT = {
|
|
68
|
+
"print": {
|
|
69
|
+
"sequence_id": "0",
|
|
70
|
+
"command": "gcode_line",
|
|
71
|
+
"param": "G91\nG1 X100 F3600\n"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
MOVE_LEFT = {
|
|
76
|
+
"print": {
|
|
77
|
+
"sequence_id": "0",
|
|
78
|
+
"command": "gcode_line",
|
|
79
|
+
"param": "G91\nG1 X-100 F3600\n"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
UNLOAD_FILAMENT = {
|
|
84
|
+
"print": {
|
|
85
|
+
"sequence_id": "0",
|
|
86
|
+
"command": "unload_filament"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
AMS_FILAMENT_CHANGE = {
|
|
91
|
+
"print": {
|
|
92
|
+
"sequence_id": "0",
|
|
93
|
+
"command": "ams_change_filament",
|
|
94
|
+
"target": 0,
|
|
95
|
+
"curr_temp": 250,
|
|
96
|
+
"tar_temp": 250
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
PRINT_3MF_FILE = {
|
|
101
|
+
"print": {
|
|
102
|
+
"ams_mapping": [],
|
|
103
|
+
"bed_leveling": True,
|
|
104
|
+
"bed_type": "hot_plate",
|
|
105
|
+
"command": "project_file",
|
|
106
|
+
"file": "Oreo.gcode.3mf",
|
|
107
|
+
"flow_cali": True,
|
|
108
|
+
"layer_inspect": True,
|
|
109
|
+
"md5": "",
|
|
110
|
+
"param": "Metadata/plate_1.gcode",
|
|
111
|
+
"profile_id": "0",
|
|
112
|
+
"project_id": "0",
|
|
113
|
+
"reason": "success",
|
|
114
|
+
"result": "success",
|
|
115
|
+
"sequence_id": "0",
|
|
116
|
+
"subtask_id": "0",
|
|
117
|
+
"subtask_name": "Oreo",
|
|
118
|
+
"task_id": "0",
|
|
119
|
+
"timelapse": False,
|
|
120
|
+
"url": "file:///sdcard/Oreo.gcode.3mf",
|
|
121
|
+
"use_ams": False,
|
|
122
|
+
"vibration_cali": False
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# X1 only currently
|
|
127
|
+
GET_ACCESSORIES = {"system": {"sequence_id": "0", "command": "get_accessories", "accessory_type": "none"}}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import atexit
|
|
3
|
+
import logging.config
|
|
4
|
+
import logging.handlers
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("bambuprinter")
|
|
8
|
+
|
|
9
|
+
class BambuConfig:
|
|
10
|
+
def __init__(self, hostname=None,
|
|
11
|
+
access_code=None,
|
|
12
|
+
serial_number=None,
|
|
13
|
+
mqtt_port=8883,
|
|
14
|
+
mqtt_client_id="studio_client_id:0c1f",
|
|
15
|
+
mqtt_username="bblp",
|
|
16
|
+
verbose=False):
|
|
17
|
+
|
|
18
|
+
setup_logging()
|
|
19
|
+
|
|
20
|
+
self.hostname = hostname
|
|
21
|
+
self.access_code = access_code
|
|
22
|
+
self.serial_number = serial_number
|
|
23
|
+
self.mqtt_port = mqtt_port
|
|
24
|
+
self.mqtt_client_id = mqtt_client_id
|
|
25
|
+
self.mqtt_username = mqtt_username
|
|
26
|
+
self.firmware_version = "N/A"
|
|
27
|
+
self.ams_firmware_version = "N/A"
|
|
28
|
+
self.verbose = verbose
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def hostname(self):
|
|
32
|
+
return self._hostname
|
|
33
|
+
@hostname.setter
|
|
34
|
+
def hostname(self, value):
|
|
35
|
+
self._hostname = value
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def access_code(self):
|
|
39
|
+
return self._access_code
|
|
40
|
+
@access_code.setter
|
|
41
|
+
def access_code(self, value):
|
|
42
|
+
self._access_code = value
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def serial_number(self):
|
|
46
|
+
return self._serial_number
|
|
47
|
+
@serial_number.setter
|
|
48
|
+
def serial_number(self, value):
|
|
49
|
+
self._serial_number = value
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def mqtt_port(self):
|
|
53
|
+
return self._mqtt_port
|
|
54
|
+
@mqtt_port.setter
|
|
55
|
+
def mqtt_port(self, value):
|
|
56
|
+
self._mqtt_port = value
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def mqtt_client_id(self):
|
|
60
|
+
return self._mqtt_client_id
|
|
61
|
+
@mqtt_client_id.setter
|
|
62
|
+
def mqtt_client_id(self, value):
|
|
63
|
+
self._mqtt_client_id = value
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def mqtt_username(self):
|
|
67
|
+
return self._mqtt_username
|
|
68
|
+
@mqtt_username.setter
|
|
69
|
+
def mqtt_username(self, value):
|
|
70
|
+
self._mqtt_username = value
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def firmware_version(self):
|
|
74
|
+
return self._firmware_version
|
|
75
|
+
@firmware_version.setter
|
|
76
|
+
def firmware_version(self, value):
|
|
77
|
+
self._firmware_version = value
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def ams_firmware_version(self):
|
|
81
|
+
return self._ams_firmware_version
|
|
82
|
+
@ams_firmware_version.setter
|
|
83
|
+
def ams_firmware_version(self, value):
|
|
84
|
+
self._ams_firmware_version = value
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def verbose(self):
|
|
88
|
+
return self._verbose
|
|
89
|
+
@verbose.setter
|
|
90
|
+
def verbose(self, value):
|
|
91
|
+
self._verbose = value
|
|
92
|
+
handler = logging.getHandlerByName("stderr")
|
|
93
|
+
if self._verbose:
|
|
94
|
+
handler.setLevel(logging.DEBUG)
|
|
95
|
+
else:
|
|
96
|
+
handler.setLevel(logging.WARN)
|
|
97
|
+
logger.info("log level changed", extra={"new_level": logging.getLevelName(handler.level)})
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def setup_logging():
|
|
101
|
+
config_file = os.path.dirname(os.path.realpath(__file__)) + "/bambuprinterlogger.json"
|
|
102
|
+
with open(config_file) as f_in:
|
|
103
|
+
config = json.load(f_in)
|
|
104
|
+
|
|
105
|
+
logging.config.dictConfig(config)
|
|
106
|
+
queue_handler = logging.getHandlerByName("queue_handler")
|
|
107
|
+
if queue_handler is not None:
|
|
108
|
+
queue_handler.listener.start()
|
|
109
|
+
atexit.register(queue_handler.listener.stop)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from typing import override
|
|
5
|
+
|
|
6
|
+
LOG_RECORD_BUILTIN_ATTRS = {
|
|
7
|
+
"args",
|
|
8
|
+
"asctime",
|
|
9
|
+
"created",
|
|
10
|
+
"exc_info",
|
|
11
|
+
"exc_text",
|
|
12
|
+
"filename",
|
|
13
|
+
"funcName",
|
|
14
|
+
"levelname",
|
|
15
|
+
"levelno",
|
|
16
|
+
"lineno",
|
|
17
|
+
"module",
|
|
18
|
+
"msecs",
|
|
19
|
+
"message",
|
|
20
|
+
"msg",
|
|
21
|
+
"name",
|
|
22
|
+
"pathname",
|
|
23
|
+
"process",
|
|
24
|
+
"processName",
|
|
25
|
+
"relativeCreated",
|
|
26
|
+
"stack_info",
|
|
27
|
+
"thread",
|
|
28
|
+
"threadName",
|
|
29
|
+
"taskName",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BambuJSONFormatter(logging.Formatter):
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
fmt_keys: dict[str, str] | None = None,
|
|
38
|
+
):
|
|
39
|
+
super().__init__()
|
|
40
|
+
self.fmt_keys = fmt_keys if fmt_keys is not None else {}
|
|
41
|
+
|
|
42
|
+
@override
|
|
43
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
44
|
+
message = self._prepare_log_dict(record)
|
|
45
|
+
return json.dumps(message, default=str)
|
|
46
|
+
|
|
47
|
+
def _prepare_log_dict(self, record: logging.LogRecord):
|
|
48
|
+
always_fields = {
|
|
49
|
+
"message": record.getMessage(),
|
|
50
|
+
"timestamp": dt.datetime.fromtimestamp(
|
|
51
|
+
record.created, tz=dt.timezone.utc
|
|
52
|
+
).isoformat(),
|
|
53
|
+
}
|
|
54
|
+
if record.exc_info is not None:
|
|
55
|
+
always_fields["exc_info"] = self.formatException(record.exc_info).replace("\\\\", "\\")
|
|
56
|
+
|
|
57
|
+
if record.stack_info is not None:
|
|
58
|
+
always_fields["stack_info"] = self.formatStack(record.stack_info).replace("\\\\", "\\")
|
|
59
|
+
|
|
60
|
+
message = {
|
|
61
|
+
key: msg_val
|
|
62
|
+
if (msg_val := always_fields.pop(val, None)) is not None
|
|
63
|
+
else getattr(record, val)
|
|
64
|
+
for key, val in self.fmt_keys.items()
|
|
65
|
+
}
|
|
66
|
+
message.update(always_fields)
|
|
67
|
+
|
|
68
|
+
for key, val in record.__dict__.items():
|
|
69
|
+
if key not in LOG_RECORD_BUILTIN_ATTRS:
|
|
70
|
+
message[key] = val
|
|
71
|
+
|
|
72
|
+
return message
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class NonErrorFilter(logging.Filter):
|
|
76
|
+
@override
|
|
77
|
+
def filter(self, record: logging.LogRecord) -> bool | logging.LogRecord:
|
|
78
|
+
return record.levelno <= logging.INFO
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from webcolors import hex_to_name
|
|
3
|
+
import paho.mqtt.client as mqtt
|
|
4
|
+
import threading
|
|
5
|
+
import ssl
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from bambucommands import *
|
|
9
|
+
from bambuspool import BambuSpool
|
|
10
|
+
from bambutools import PrinterState
|
|
11
|
+
from bambutools import parseStage
|
|
12
|
+
from bambuconfig import BambuConfig
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import atexit
|
|
16
|
+
import logging.config
|
|
17
|
+
import logging.handlers
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("bambuprinter")
|
|
20
|
+
|
|
21
|
+
class BambuPrinter:
|
|
22
|
+
def __init__(self, config=BambuConfig()):
|
|
23
|
+
setup_logging()
|
|
24
|
+
|
|
25
|
+
self._internalException = None
|
|
26
|
+
self._lastMessageTime = None
|
|
27
|
+
|
|
28
|
+
self._config = config
|
|
29
|
+
self._state = PrinterState
|
|
30
|
+
|
|
31
|
+
self._client = None
|
|
32
|
+
self._on_update = None
|
|
33
|
+
|
|
34
|
+
self._bed_temp = 0.0
|
|
35
|
+
self._bed_temp_target = 0.0
|
|
36
|
+
self._tool_temp = 0.0
|
|
37
|
+
self._tool_temp_target = 0.0
|
|
38
|
+
self._chamber_temp = 0.0
|
|
39
|
+
self._chamber_temp_target = 0.0
|
|
40
|
+
|
|
41
|
+
self._fan_gear = 0
|
|
42
|
+
self._heatbreak_fan_speed = 0
|
|
43
|
+
self._fan_speed = 0
|
|
44
|
+
|
|
45
|
+
self._light_state = "N/A"
|
|
46
|
+
self._wifi_signal = "N/A"
|
|
47
|
+
self._speed_level = 0
|
|
48
|
+
|
|
49
|
+
self._gcode_state = "N/A"
|
|
50
|
+
self._gcode_file = "N/A"
|
|
51
|
+
self._print_type = "N/A"
|
|
52
|
+
self._percent_complete = 0
|
|
53
|
+
self._time_remaining = 0
|
|
54
|
+
self._layer_count = 0
|
|
55
|
+
self._current_layer = 0
|
|
56
|
+
|
|
57
|
+
self._current_stage = 0
|
|
58
|
+
self._current_stage_text = "N/A"
|
|
59
|
+
|
|
60
|
+
self._spools = ()
|
|
61
|
+
self._target_spool = 255
|
|
62
|
+
self._active_spool = 255
|
|
63
|
+
self._spool_state = "N/A"
|
|
64
|
+
self._ams_status = None
|
|
65
|
+
|
|
66
|
+
def start_session(self):
|
|
67
|
+
logger.debug("session start_session")
|
|
68
|
+
if self.config.hostname is None or self.config.access_code is None or self.config.serial_number is None:
|
|
69
|
+
raise Exception("hostname, access_code, and serial_number are required")
|
|
70
|
+
if self.client and self.client.is_connected():
|
|
71
|
+
raise Exception("a session is already active")
|
|
72
|
+
|
|
73
|
+
def on_connect(client, userdata, flags, rc):
|
|
74
|
+
logger.debug("session on_connect")
|
|
75
|
+
if self.state != PrinterState.PAUSED:
|
|
76
|
+
self.state = PrinterState.CONNECTED
|
|
77
|
+
client.subscribe(f"device/{self.config.serial_number}/report")
|
|
78
|
+
logger.debug(f"subscribed to [device/{self.config.serial_number}/report]")
|
|
79
|
+
def on_disconnect(client, userdata, rc):
|
|
80
|
+
logger.debug("session on_disconnect")
|
|
81
|
+
if self._internalException:
|
|
82
|
+
logger.exception("an internal exception occurred")
|
|
83
|
+
self.state = PrinterState.QUIT
|
|
84
|
+
raise self._internalException
|
|
85
|
+
if self.state != PrinterState.PAUSED:
|
|
86
|
+
self.state = PrinterState.DISCONNECTED
|
|
87
|
+
def on_message(client, userdata, msg):
|
|
88
|
+
if self._lastMessageTime: self._lastMessageTime = time.time()
|
|
89
|
+
self._on_message(json.loads(msg.payload.decode("utf-8")))
|
|
90
|
+
def loop_forever(printer):
|
|
91
|
+
try:
|
|
92
|
+
printer.client.loop_forever(retry_first_connection=True)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.exception("an internal exception occurred")
|
|
95
|
+
printer._internalException = e
|
|
96
|
+
if printer.client and printer.client.is_connected(): printer.client.disconnect()
|
|
97
|
+
printer.state = PrinterState.QUIT
|
|
98
|
+
|
|
99
|
+
self.client = mqtt.Client()
|
|
100
|
+
|
|
101
|
+
self.client.on_connect = on_connect
|
|
102
|
+
self.client.on_disconnect = on_disconnect
|
|
103
|
+
self.client.on_message = on_message
|
|
104
|
+
|
|
105
|
+
self.client.tls_set(tls_version=ssl.PROTOCOL_TLS, cert_reqs=ssl.CERT_NONE)
|
|
106
|
+
self.client.tls_insecure_set(True)
|
|
107
|
+
self.client.reconnect_delay_set(min_delay=1, max_delay=1)
|
|
108
|
+
|
|
109
|
+
self.client.username_pw_set(self.config.mqtt_username, password=self.config.access_code)
|
|
110
|
+
self.client.user_data_set(self.config.mqtt_client_id)
|
|
111
|
+
|
|
112
|
+
self.client.connect(self.config.hostname, self.config.mqtt_port, 60)
|
|
113
|
+
threading.Thread(target=loop_forever, name="bambuprinter-session", args=(self,)).start()
|
|
114
|
+
|
|
115
|
+
self._start_watchdog()
|
|
116
|
+
|
|
117
|
+
def pause_session(self):
|
|
118
|
+
if self.state != PrinterState.PAUSED:
|
|
119
|
+
self.client.unsubscribe(f"device/{self.config.serial_number}/report")
|
|
120
|
+
logger.debug(f"unsubscribed from [device/{self.config.serial_number}/report]")
|
|
121
|
+
self.state = PrinterState.PAUSED
|
|
122
|
+
|
|
123
|
+
def resume_session(self):
|
|
124
|
+
if self.client and self.client.is_connected() and self.state == PrinterState.PAUSED:
|
|
125
|
+
self.client.subscribe(f"device/{self.config.serial_number}/report")
|
|
126
|
+
logger.debug(f"subscribed to [device/{self.config.serial_number}/report]")
|
|
127
|
+
self._lastMessageTime = time.time()
|
|
128
|
+
self.state = PrinterState.CONNECTED
|
|
129
|
+
return
|
|
130
|
+
self.state = PrinterState.QUIT
|
|
131
|
+
|
|
132
|
+
def quit(self):
|
|
133
|
+
if self.client and self.client.is_connected():
|
|
134
|
+
self.client.disconnect()
|
|
135
|
+
while self.state != PrinterState.QUIT:
|
|
136
|
+
time.sleep(.1)
|
|
137
|
+
logger.debug("mqtt client was connected")
|
|
138
|
+
else:
|
|
139
|
+
self.state == PrinterState.QUIT
|
|
140
|
+
logger.debug("mqtt client was already disconnected")
|
|
141
|
+
|
|
142
|
+
def refresh(self):
|
|
143
|
+
if self.state == PrinterState.CONNECTED:
|
|
144
|
+
self.client.publish(f"device/{self.config.serial_number}/request", json.dumps(ANNOUNCE_PUSH))
|
|
145
|
+
logger.debug(f"published ANNOUNCE_PUSH to [device/{self.config.serial_number}/request]")
|
|
146
|
+
self.client.publish(f"device/{self.config.serial_number}/request", json.dumps(ANNOUNCE_VERSION))
|
|
147
|
+
logger.debug(f"published ANNOUNCE_VERSION to [device/{self.config.serial_number}/request]")
|
|
148
|
+
|
|
149
|
+
def unload_filament(self):
|
|
150
|
+
self.client.publish(f"device/{self.config.serial_number}/request", json.dumps(UNLOAD_FILAMENT))
|
|
151
|
+
logger.debug(f"published UNLOAD_FILAMENT to [device/{self.config.serial_number}/request]")
|
|
152
|
+
|
|
153
|
+
def load_filament(self, slot):
|
|
154
|
+
msg = AMS_FILAMENT_CHANGE
|
|
155
|
+
msg["print"]["target"] = int(slot)
|
|
156
|
+
self.client.publish(f"device/{self.config.serial_number}/request", json.dumps(msg))
|
|
157
|
+
logger.debug(f"published AMS_FILAMENT_CHANGE to [device/{self.config.serial_number}/request]", extra={"target": slot})
|
|
158
|
+
|
|
159
|
+
def send_gcode(self, gcode):
|
|
160
|
+
cmd = SEND_GCODE_TEMPLATE
|
|
161
|
+
cmd["print"]["param"] = f"{gcode} \n"
|
|
162
|
+
self.client.publish(f"device/{self.config.serial_number}/request", json.dumps(cmd))
|
|
163
|
+
logger.debug(f"published SEND_GCODE_TEMPLATE to [device/{self.config.serial_number}/request]", extra={"gcode": gcode})
|
|
164
|
+
|
|
165
|
+
def print_3mf_file(self, name, bed, ams):
|
|
166
|
+
file = PRINT_3MF_FILE
|
|
167
|
+
file["print"]["file"] = f"{name}.gcode.3mf"
|
|
168
|
+
file["print"]["url"] = f"file:///sdcard/jobs/{name}.gcode.3mf"
|
|
169
|
+
file["print"]["subtask_name"] = name
|
|
170
|
+
if bed == "1":
|
|
171
|
+
file["print"]["bed_type"] = "hot_plate"
|
|
172
|
+
else:
|
|
173
|
+
file["print"]["bed_type"] = "textured_plate"
|
|
174
|
+
if len(ams) > 2:
|
|
175
|
+
file["print"]["use_ams"] = True
|
|
176
|
+
file["print"]["ams_mapping"] = json.loads(ams)
|
|
177
|
+
else:
|
|
178
|
+
file["print"]["use_ams"] = False
|
|
179
|
+
file["print"]["ams_mapping"] = ""
|
|
180
|
+
self.client.publish(f"device/{self.config.serial_number}/request", json.dumps(file))
|
|
181
|
+
logger.debug(f"published PRINT_3MF_FILE to [device/{self.config.serial_number}/request]", extra={"3mf_name": name, "bed": bed, "ams": ams})
|
|
182
|
+
|
|
183
|
+
def stop_printing(self):
|
|
184
|
+
self.client.publish(f"device/{self.config.serial_number}/request", json.dumps(STOP_PRINT))
|
|
185
|
+
logger.debug(f"published STOP_PRINT to [device/{self.config.serial_number}/request]")
|
|
186
|
+
|
|
187
|
+
def pause_printing(self):
|
|
188
|
+
self.client.publish(f"device/{self.config.serial_number}/request", json.dumps(PAUSE_PRINT))
|
|
189
|
+
logger.debug(f"published PAUSE_PRINT to [device/{self.config.serial_number}/request]")
|
|
190
|
+
|
|
191
|
+
def resume_printing(self):
|
|
192
|
+
self.client.publish(f"device/{self.config.serial_number}/request", json.dumps(RESUME_PRINT))
|
|
193
|
+
logger.debug(f"published RESUME_PRINT to [device/{self.config.serial_number}/request]")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def jsonSerializer(self, obj):
|
|
197
|
+
try:
|
|
198
|
+
if isinstance(obj, mqtt.Client):
|
|
199
|
+
return ""
|
|
200
|
+
if str(obj.__class__).replace("<class '", "").replace("'>", "") == "mappingproxy":
|
|
201
|
+
return "bambutools.PrinterState"
|
|
202
|
+
return obj.__dict__
|
|
203
|
+
except Exception as e:
|
|
204
|
+
logger.warn("unable to serialize object", extra={"obj": obj})
|
|
205
|
+
return "not available"
|
|
206
|
+
|
|
207
|
+
def _start_watchdog(self):
|
|
208
|
+
def watchdog_thread(printer):
|
|
209
|
+
try:
|
|
210
|
+
while printer.state != PrinterState.QUIT:
|
|
211
|
+
if printer.state == PrinterState.CONNECTED and (printer._lastMessageTime is None or printer._lastMessageTime + 15 < time.time()):
|
|
212
|
+
if printer._lastMessageTime: logger.warn("BambuPrinter watchdog timeout")
|
|
213
|
+
printer.client.publish(f"device/{printer.config.serial_number}/request", json.dumps(ANNOUNCE_PUSH))
|
|
214
|
+
printer.client.publish(f"device/{printer.config.serial_number}/request", json.dumps(ANNOUNCE_VERSION))
|
|
215
|
+
printer._lastMessageTime = time.time()
|
|
216
|
+
time.sleep(.1)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.exception("an internal exception occurred")
|
|
219
|
+
printer._internalException = e
|
|
220
|
+
if printer.client and printer.client.is_connected(): printer.client.disconnect()
|
|
221
|
+
|
|
222
|
+
threading.Thread(target=watchdog_thread, name="bambuprinter-session-watchdog", args=(self,)).start()
|
|
223
|
+
|
|
224
|
+
def _on_message(self, message):
|
|
225
|
+
if "system" in message:
|
|
226
|
+
system = message["system"]
|
|
227
|
+
|
|
228
|
+
elif "print" in message:
|
|
229
|
+
status = message["print"]
|
|
230
|
+
|
|
231
|
+
if "bed_temper" in status: self._bed_temp = float(status["bed_temper"])
|
|
232
|
+
if "bed_target_temper" in status: self._bed_temp_target = float(status["bed_target_temper"])
|
|
233
|
+
if "nozzle_temper" in status: self._tool_temp = float(status["nozzle_temper"])
|
|
234
|
+
if "nozzle_target_temper" in status: self._tool_temp_target = float(status["nozzle_target_temper"])
|
|
235
|
+
|
|
236
|
+
if "fan_gear" in status: self._fan_gear = int(status["fan_gear"])
|
|
237
|
+
if "heatbreak_fan_speed" in status: self._heatbreak_fan_speed = int(status["heatbreak_fan_speed"])
|
|
238
|
+
if "cooling_fan_speed" in status: self._fan_speed = int(status["cooling_fan_speed"])
|
|
239
|
+
|
|
240
|
+
if "wifi_signal" in status: self._wifi_signal = status["wifi_signal"]
|
|
241
|
+
if "lights_report" in status: self._light_state = (status["lights_report"])[0]["mode"]
|
|
242
|
+
if "spd_lvl" in status: self._speed_level = status["spd_lvl"]
|
|
243
|
+
if "gcode_state" in status: self._gcode_state = status["gcode_state"]
|
|
244
|
+
if "gcode_file" in status: self._gcode_file = status["gcode_file"]
|
|
245
|
+
if "print_type" in status: self._print_type = status["print_type"]
|
|
246
|
+
if "mc_percent" in status: self._percent_complete = status["mc_percent"]
|
|
247
|
+
if "mc_remaining_time" in status: self._time_remaining = status["mc_remaining_time"]
|
|
248
|
+
if "total_layer_num" in status: self._layer_count = status["total_layer_num"]
|
|
249
|
+
if "layer_num" in status: self._current_layer = status["layer_num"]
|
|
250
|
+
|
|
251
|
+
if "stg_cur" in status:
|
|
252
|
+
self._current_stage = int(status["stg_cur"])
|
|
253
|
+
self._current_stage_text = parseStage(self._current_stage)
|
|
254
|
+
|
|
255
|
+
if "command" in status and status["command"] == "project_file":
|
|
256
|
+
print(json.dumps(message, indent=4, sort_keys=True).replace("\n", "\r\n"))
|
|
257
|
+
|
|
258
|
+
if "ams" in status and "ams" in status["ams"]:
|
|
259
|
+
spools = []
|
|
260
|
+
ams = (status["ams"]["ams"])[0]
|
|
261
|
+
# print(json.dumps(status, indent=4, sort_keys=True).replace("\n", "\r\n"))
|
|
262
|
+
for tray in ams["tray"]:
|
|
263
|
+
try:
|
|
264
|
+
tray_color = hex_to_name("#" + tray["tray_color"][:6])
|
|
265
|
+
except:
|
|
266
|
+
try:
|
|
267
|
+
tray_color = tray["tray_color"]
|
|
268
|
+
except:
|
|
269
|
+
tray_color = "N/A"
|
|
270
|
+
|
|
271
|
+
spool = BambuSpool(int(tray["id"]), tray["tray_id_name"] if "tray_id_name" in tray else "", tray["tray_type"] if "tray_type" in tray else "", tray["tray_sub_brands"] if "tray_sub_brands" in tray else "", tray_color)
|
|
272
|
+
spools.append(spool)
|
|
273
|
+
self._spools = tuple(spools)
|
|
274
|
+
|
|
275
|
+
if "vt_tray" in status:
|
|
276
|
+
tray = status["vt_tray"]
|
|
277
|
+
try:
|
|
278
|
+
tray_color = hex_to_name("#" + tray["tray_color"][:6])
|
|
279
|
+
except:
|
|
280
|
+
try:
|
|
281
|
+
tray_color = tray["tray_color"]
|
|
282
|
+
except:
|
|
283
|
+
tray_color = "N/A"
|
|
284
|
+
|
|
285
|
+
spool = BambuSpool(int(tray["id"]), tray["tray_id_name"], tray["tray_type"], tray["tray_sub_brands"], tray_color)
|
|
286
|
+
|
|
287
|
+
if range(len(self.spools), 1, 2):
|
|
288
|
+
spools = (spool,)
|
|
289
|
+
else:
|
|
290
|
+
spools = list(self.spools)
|
|
291
|
+
spools.append(spool)
|
|
292
|
+
self._spools = tuple(spools)
|
|
293
|
+
|
|
294
|
+
tray_tar = None
|
|
295
|
+
tray_now = None
|
|
296
|
+
|
|
297
|
+
if "ams" in status and "tray_tar" in status["ams"]:
|
|
298
|
+
tray_tar = int(status["ams"]["tray_tar"])
|
|
299
|
+
if tray_tar != 255:
|
|
300
|
+
self._target_spool = int(tray_tar)
|
|
301
|
+
|
|
302
|
+
if "ams" in status and "tray_now" in status["ams"]:
|
|
303
|
+
tray_now = int(status["ams"]["tray_now"])
|
|
304
|
+
if tray_now != 255:
|
|
305
|
+
if self.active_spool != tray_now:
|
|
306
|
+
self._spool_state = f"Loading"
|
|
307
|
+
self._active_spool = tray_now
|
|
308
|
+
|
|
309
|
+
if not tray_tar is None and tray_tar != tray_now:
|
|
310
|
+
self._spool_state = f"Unloading"
|
|
311
|
+
if not tray_now is None: self._active_spool = tray_now
|
|
312
|
+
|
|
313
|
+
if "ams" in status and "tray_pre" in status["ams"]:
|
|
314
|
+
tray_pre = int(status["ams"]["tray_pre"])
|
|
315
|
+
if self.spool_state == "Unloading":
|
|
316
|
+
self._spool_state = "Unloaded"
|
|
317
|
+
|
|
318
|
+
if "ams_status" in status:
|
|
319
|
+
self._ams_status = int(status["ams_status"])
|
|
320
|
+
if self._ams_status == 768:
|
|
321
|
+
self._spool_state = "Loaded"
|
|
322
|
+
|
|
323
|
+
elif "info" in message and "result" in message["info"] and message["info"]["result"] == "success":
|
|
324
|
+
info = message["info"]
|
|
325
|
+
for module in info["module"]:
|
|
326
|
+
if "ota" in module["name"]:
|
|
327
|
+
self.config.serial_number = module["sn"]
|
|
328
|
+
self.config.firmware_version = module["sw_ver"]
|
|
329
|
+
if "ams" in module["name"]:
|
|
330
|
+
self.config.ams_firmware_version = module["sw_ver"]
|
|
331
|
+
else:
|
|
332
|
+
print(json.dumps(message, indent=4, sort_keys=True).replace("\n", "\r\n"))
|
|
333
|
+
|
|
334
|
+
if self.on_update: self.on_update(self)
|
|
335
|
+
|
|
336
|
+
logger.debug("message processed", extra={"bambu_msg": message})
|
|
337
|
+
if self.config.verbose:
|
|
338
|
+
print("\r" + json.dumps(message, indent=4, sort_keys=True).replace("\n", "\r\n") + "\r")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def config(self):
|
|
343
|
+
return self._config
|
|
344
|
+
@config.setter
|
|
345
|
+
def config(self, value):
|
|
346
|
+
self._config = value
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def state(self):
|
|
350
|
+
return self._state
|
|
351
|
+
@state.setter
|
|
352
|
+
def state(self, value):
|
|
353
|
+
self._state = value
|
|
354
|
+
|
|
355
|
+
@property
|
|
356
|
+
def client(self):
|
|
357
|
+
return self._client
|
|
358
|
+
@client.setter
|
|
359
|
+
def client(self, value):
|
|
360
|
+
self._client = value
|
|
361
|
+
|
|
362
|
+
@property
|
|
363
|
+
def on_update(self):
|
|
364
|
+
return self._on_update
|
|
365
|
+
@on_update.setter
|
|
366
|
+
def on_update(self, value):
|
|
367
|
+
self._on_update = value
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def bed_temp(self):
|
|
371
|
+
return self._bed_temp
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def bed_temp_target(self):
|
|
375
|
+
return self._bed_temp_target
|
|
376
|
+
@bed_temp_target.setter
|
|
377
|
+
def bed_temp_target(self, value):
|
|
378
|
+
gcode = SEND_GCODE_TEMPLATE
|
|
379
|
+
gcode["print"]["param"] = f"M140 S{value}\n"
|
|
380
|
+
self.client.publish(f"device/{self.config.serial_number}/request", json.dumps(gcode))
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def tool_temp(self):
|
|
384
|
+
return self._tool_temp
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def tool_temp_target(self):
|
|
388
|
+
return self._tool_temp_target
|
|
389
|
+
@tool_temp_target.setter
|
|
390
|
+
def tool_temp_target(self, value):
|
|
391
|
+
gcode = SEND_GCODE_TEMPLATE
|
|
392
|
+
gcode["print"]["param"] = f"M104 S{value}\n"
|
|
393
|
+
self.client.publish(f"device/{self.config.serial_number}/request", json.dumps(gcode))
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def chamber_temp(self):
|
|
397
|
+
return self._chamber_temp
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def chamber_temp_target(self):
|
|
401
|
+
return self._chamber_temp_target
|
|
402
|
+
@chamber_temp.setter
|
|
403
|
+
def chamber_temp_target(self, value):
|
|
404
|
+
self._chamber_temp_target = value
|
|
405
|
+
|
|
406
|
+
@property
|
|
407
|
+
def fan_speed(self):
|
|
408
|
+
return self._fan_speed
|
|
409
|
+
@fan_speed.setter
|
|
410
|
+
def fan_speed(self, value):
|
|
411
|
+
speed = round(value * 2.55, 0)
|
|
412
|
+
gcode = SEND_GCODE_TEMPLATE
|
|
413
|
+
gcode["print"]["param"] = f"M106 P1 S{speed}\nM106 P2 S{speed}\nM106 P3 S{speed}\n"
|
|
414
|
+
self.client.publish(f"device/{self.config.serial_number}/request", json.dumps(gcode))
|
|
415
|
+
|
|
416
|
+
@property
|
|
417
|
+
def fan_gear(self):
|
|
418
|
+
return self._fan_gear
|
|
419
|
+
|
|
420
|
+
@property
|
|
421
|
+
def heatbreak_fan_speed(self):
|
|
422
|
+
return self._heatbreak_fan_speed
|
|
423
|
+
|
|
424
|
+
@property
|
|
425
|
+
def wifi_signal(self):
|
|
426
|
+
return self._wifi_signal
|
|
427
|
+
|
|
428
|
+
@property
|
|
429
|
+
def light_state(self):
|
|
430
|
+
return self._light_state == "on"
|
|
431
|
+
@light_state.setter
|
|
432
|
+
def light_state(self, value):
|
|
433
|
+
cmd = CHAMBER_LIGHT_TOGGLE
|
|
434
|
+
if value:
|
|
435
|
+
cmd["system"]["led_mode"] = "on"
|
|
436
|
+
else:
|
|
437
|
+
cmd["system"]["led_mode"] = "off"
|
|
438
|
+
self.client.publish(f"device/{self.config.serial_number}/request", json.dumps(cmd))
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def speed_level(self):
|
|
442
|
+
return self._speed_level
|
|
443
|
+
@speed_level.setter
|
|
444
|
+
def speed_level(self, value):
|
|
445
|
+
cmd = SPEED_PROFILE_TEMPLATE
|
|
446
|
+
cmd["print"]["param"] = value
|
|
447
|
+
self.client.publish(f"device/{self.config.serial_number}/request", json.dumps(cmd))
|
|
448
|
+
|
|
449
|
+
@property
|
|
450
|
+
def gcode_state(self):
|
|
451
|
+
return self._gcode_state
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def gcode_file(self):
|
|
455
|
+
return self._gcode_file
|
|
456
|
+
@gcode_file.setter
|
|
457
|
+
def gcode_file(self, value):
|
|
458
|
+
self._gcode_file = value
|
|
459
|
+
|
|
460
|
+
@property
|
|
461
|
+
def print_type(self):
|
|
462
|
+
return self._print_type
|
|
463
|
+
|
|
464
|
+
@property
|
|
465
|
+
def percent_complete(self):
|
|
466
|
+
return self._percent_complete
|
|
467
|
+
|
|
468
|
+
@property
|
|
469
|
+
def time_remaining(self):
|
|
470
|
+
return self._time_remaining
|
|
471
|
+
|
|
472
|
+
@property
|
|
473
|
+
def layer_count(self):
|
|
474
|
+
return self._layer_count
|
|
475
|
+
|
|
476
|
+
@property
|
|
477
|
+
def current_layer(self):
|
|
478
|
+
return self._current_layer
|
|
479
|
+
|
|
480
|
+
@property
|
|
481
|
+
def current_stage(self):
|
|
482
|
+
return self._current_stage
|
|
483
|
+
|
|
484
|
+
@property
|
|
485
|
+
def current_stage_text(self):
|
|
486
|
+
return parseStage(self._current_stage)
|
|
487
|
+
|
|
488
|
+
@property
|
|
489
|
+
def spools(self):
|
|
490
|
+
return self._spools
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def target_spool(self):
|
|
494
|
+
return self._target_spool
|
|
495
|
+
|
|
496
|
+
@property
|
|
497
|
+
def active_spool(self):
|
|
498
|
+
return self._active_spool
|
|
499
|
+
|
|
500
|
+
@property
|
|
501
|
+
def spool_state(self):
|
|
502
|
+
return self._spool_state
|
|
503
|
+
|
|
504
|
+
@property
|
|
505
|
+
def ams_status(self):
|
|
506
|
+
return self._ams_status
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def setup_logging():
|
|
510
|
+
config_file = os.path.dirname(os.path.realpath(__file__)) + "/bambuprinterlogger.json"
|
|
511
|
+
with open(config_file) as f_in:
|
|
512
|
+
config = json.load(f_in)
|
|
513
|
+
|
|
514
|
+
logging.config.dictConfig(config)
|
|
515
|
+
queue_handler = logging.getHandlerByName("queue_handler")
|
|
516
|
+
if queue_handler is not None:
|
|
517
|
+
queue_handler.listener.start()
|
|
518
|
+
atexit.register(queue_handler.listener.stop)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"disable_existing_loggers": false,
|
|
4
|
+
"formatters": {
|
|
5
|
+
"simple": {
|
|
6
|
+
"format": "%(levelname)s: %(message)s",
|
|
7
|
+
"datefmt": "%Y-%m-%dT%H:%M:%S%z"
|
|
8
|
+
},
|
|
9
|
+
"json": {
|
|
10
|
+
"()": "bambulogger.BambuJSONFormatter",
|
|
11
|
+
"fmt_keys": {
|
|
12
|
+
"level": "levelname",
|
|
13
|
+
"message": "message",
|
|
14
|
+
"timestamp": "timestamp",
|
|
15
|
+
"logger": "name",
|
|
16
|
+
"module": "module",
|
|
17
|
+
"function": "funcName",
|
|
18
|
+
"line": "lineno",
|
|
19
|
+
"thread_name": "threadName"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"handlers": {
|
|
24
|
+
"stderr": {
|
|
25
|
+
"class": "logging.StreamHandler",
|
|
26
|
+
"level": "WARNING",
|
|
27
|
+
"formatter": "simple",
|
|
28
|
+
"stream": "ext://sys.stderr"
|
|
29
|
+
},
|
|
30
|
+
"file": {
|
|
31
|
+
"class": "logging.handlers.RotatingFileHandler",
|
|
32
|
+
"level": "DEBUG",
|
|
33
|
+
"formatter": "json",
|
|
34
|
+
"filename": "BambuPrinter.log.jsonl",
|
|
35
|
+
"maxBytes": 10000000,
|
|
36
|
+
"backupCount": 3
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"loggers": {
|
|
40
|
+
"root": {
|
|
41
|
+
"level": "DEBUG",
|
|
42
|
+
"handlers": [
|
|
43
|
+
"stderr",
|
|
44
|
+
"file"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
class BambuSpool:
|
|
2
|
+
def __repr__(self):
|
|
3
|
+
return str(self)
|
|
4
|
+
def __str__(self):
|
|
5
|
+
return (f"id=[{self.id}] name=[{self.name}] type=[{self.type}] sub brands=[{self.sub_brands}] color=[{self.color}]")
|
|
6
|
+
|
|
7
|
+
def __init__(self, id, name, type, sub_brands, color):
|
|
8
|
+
self.id = id
|
|
9
|
+
self.name = name
|
|
10
|
+
self.type = type
|
|
11
|
+
self.sub_brands = sub_brands
|
|
12
|
+
self.color = color
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def id(self):
|
|
16
|
+
return self._id
|
|
17
|
+
@id.setter
|
|
18
|
+
def id(self, value):
|
|
19
|
+
self._id = value
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def name(self):
|
|
23
|
+
return self._name
|
|
24
|
+
@name.setter
|
|
25
|
+
def name(self, value):
|
|
26
|
+
self._name = value
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def type(self):
|
|
30
|
+
return self._type
|
|
31
|
+
@type.setter
|
|
32
|
+
def type(self, value):
|
|
33
|
+
self._type = value
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def sub_brands(self):
|
|
37
|
+
return self._sub_brands
|
|
38
|
+
@sub_brands.setter
|
|
39
|
+
def sub_brands(self, value):
|
|
40
|
+
self._sub_brands = value
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def color(self):
|
|
44
|
+
return self._color
|
|
45
|
+
@color.setter
|
|
46
|
+
def color(self, value):
|
|
47
|
+
self._color = value
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
def parseStage(stage):
|
|
4
|
+
if type(stage) is int or stage.isnumeric():
|
|
5
|
+
stage = int(stage)
|
|
6
|
+
if stage == 0: return ""
|
|
7
|
+
elif stage == 1: return "Auto bed leveling"
|
|
8
|
+
elif stage == 2: return "Heatbed preheating"
|
|
9
|
+
elif stage == 3: return "Sweeping XY mech mode"
|
|
10
|
+
elif stage == 4: return "Changing filament"
|
|
11
|
+
elif stage == 5: return "M400 pause"
|
|
12
|
+
elif stage == 6: return "Paused due to filament runout"
|
|
13
|
+
elif stage == 7: return "Heating hotend"
|
|
14
|
+
elif stage == 8: return "Calibrating extrusion"
|
|
15
|
+
elif stage == 9: return "Scanning bed surface"
|
|
16
|
+
elif stage == 10: return "Inspecting first layer"
|
|
17
|
+
elif stage == 11: return "Identifying build plate type"
|
|
18
|
+
elif stage == 12: return "Calibrating Micro Lidar"
|
|
19
|
+
elif stage == 13: return "Homing toolhead"
|
|
20
|
+
elif stage == 14: return "Cleaning nozzle tip"
|
|
21
|
+
elif stage == 15: return "Checking extruder temperature"
|
|
22
|
+
elif stage == 16: return "Printing was paused by the user"
|
|
23
|
+
elif stage == 17: return "Pause of front cover falling"
|
|
24
|
+
elif stage == 18: return "Calibrating the micro lida"
|
|
25
|
+
elif stage == 19: return "Calibrating extrusion flow"
|
|
26
|
+
elif stage == 20: return "Paused due to nozzle temperature malfunction"
|
|
27
|
+
elif stage == 21: return "Paused due to heat bed temperature malfunction"
|
|
28
|
+
elif stage == 22: return "Filament unloading"
|
|
29
|
+
elif stage == 23: return "Skip step pause"
|
|
30
|
+
elif stage == 24: return "Filament loading"
|
|
31
|
+
elif stage == 25: return "Motor noise calibration"
|
|
32
|
+
elif stage == 26: return "Paused due to AMS lost"
|
|
33
|
+
elif stage == 27: return "Paused due to low speed of the heat break fan"
|
|
34
|
+
elif stage == 28: return "Paused due to chamber temperature control error"
|
|
35
|
+
elif stage == 29: return "Cooling chamber"
|
|
36
|
+
elif stage == 30: return "Paused by the Gcode inserted by user"
|
|
37
|
+
elif stage == 31: return "Motor noise showoff"
|
|
38
|
+
elif stage == 32: return "Nozzle filament covered detected pause"
|
|
39
|
+
elif stage == 33: return "Cutter error pause"
|
|
40
|
+
elif stage == 34: return "First layer error pause"
|
|
41
|
+
elif stage == 35: return "Nozzle clog pause"
|
|
42
|
+
return ""
|
|
43
|
+
|
|
44
|
+
def parseFan(fan):
|
|
45
|
+
if type(fan) is int or fan.isnumeric():
|
|
46
|
+
fan = int(fan)
|
|
47
|
+
if fan == 1: return 10
|
|
48
|
+
elif fan == 2: return 20
|
|
49
|
+
elif fan in (3, 4): return 30
|
|
50
|
+
elif fan in (5, 6): return 40
|
|
51
|
+
elif fan in (7, 8): return 50
|
|
52
|
+
elif fan == 9: return 60
|
|
53
|
+
elif fan in (10, 11): return 70
|
|
54
|
+
elif fan == 12: return 80
|
|
55
|
+
elif fan in (13, 14): return 90
|
|
56
|
+
elif fan == 15: return 100
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
class PrinterState(Enum):
|
|
60
|
+
CONNECTED = 1,
|
|
61
|
+
DISCONNECTED = 2,
|
|
62
|
+
PAUSED = 3,
|
|
63
|
+
QUIT = 4
|
|
64
|
+
|