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.
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,8 @@
1
+ # bambu-printer-manager
2
+
3
+ ## <B>Now supports Bambu printers</B>
4
+
5
+ ## Dependencies
6
+ ```
7
+ pip install paho.mqtt
8
+ ```
@@ -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"
@@ -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
+