circuitpython-bambulabs 1.0.0__py3-none-any.whl

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.
bambulabs.py ADDED
@@ -0,0 +1,306 @@
1
+ # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
2
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Paul Cutler
3
+ #
4
+ # SPDX-License-Identifier: MIT
5
+ """
6
+ `bambulabs`
7
+ ================================================================================
8
+
9
+ A library to interface with a Bambu Labs 3D printer and query status.
10
+
11
+
12
+ * Author(s): Paul Cutler
13
+
14
+ Implementation Notes
15
+ --------------------
16
+
17
+ **Hardware:**
18
+
19
+ Requires a Bambu Labs 3D printer and Bambu Labs Makerworld account.
20
+
21
+ **Software and Dependencies:**
22
+
23
+ * Adafruit CircuitPython firmware for the supported boards:
24
+ https://circuitpython.org/downloads
25
+ """
26
+
27
+ import json
28
+ import os
29
+ import time
30
+
31
+ import adafruit_connection_manager
32
+ import adafruit_minimqtt.adafruit_minimqtt as MQTT
33
+ import wifi
34
+
35
+ __version__ = "1.0.0"
36
+ __repo__ = "https://github.com/prcutler/CircuitPython_bambulabs.git"
37
+
38
+ # BAMBU MQTT settings - Bambu Cloud
39
+ BAMBU_BROKER = os.getenv("BAMBU_BROKER")
40
+ ACCESS_TOKEN = os.getenv("BAMBU_ACCESS_TOKEN")
41
+ USER_ID = os.getenv("USER_ID")
42
+ BAMBU_IP = os.getenv("BAMBU_IP")
43
+
44
+
45
+ class PrinterStatus:
46
+ """Wraps the raw JSON dict returned by a ``pushall`` response and exposes
47
+ each printer field as a named, readable property.
48
+
49
+ All values come from the snapshot captured at the time ``pushall`` was
50
+ called. Use :attr:`raw` to access the complete original dict.
51
+
52
+ :param data: The raw JSON dict received from the printer's report topic.
53
+ """
54
+
55
+ def __init__(self, data):
56
+ self._data = data
57
+ self._print = data.get("print", {})
58
+ self._info = data.get("info", {})
59
+ self._system = data.get("system", {})
60
+ self._upgrade = data.get("upgrade", {})
61
+
62
+ @property
63
+ def raw(self):
64
+ """The complete raw JSON dict from the printer."""
65
+ return self._data
66
+
67
+ # ---- Print state -------------------------------------------------------
68
+
69
+ @property
70
+ def gcode_state(self):
71
+ """Printer gcode state string (e.g. ``RUNNING``, ``PAUSE``, ``FINISH``)."""
72
+ return self._print.get("gcode_state")
73
+
74
+ @property
75
+ def print_percentage(self):
76
+ """Current print completion percentage (0–100)."""
77
+ return self._print.get("mc_percent")
78
+
79
+ @property
80
+ def remaining_time(self):
81
+ """Estimated remaining print time in minutes."""
82
+ return self._print.get("mc_remaining_time")
83
+
84
+ @property
85
+ def current_layer(self):
86
+ """Current layer number being printed."""
87
+ return self._print.get("layer_num")
88
+
89
+ @property
90
+ def total_layers(self):
91
+ """Total number of layers in the print job."""
92
+ return self._print.get("total_layer_num")
93
+
94
+ @property
95
+ def gcode_file(self):
96
+ """Filename of the currently active gcode file."""
97
+ return self._print.get("gcode_file")
98
+
99
+ @property
100
+ def subtask_name(self):
101
+ """Current print subtask / job name."""
102
+ return self._print.get("subtask_name")
103
+
104
+ @property
105
+ def print_speed(self):
106
+ """Print speed level (0=silent, 1=standard, 2=sport, 3=ludicrous)."""
107
+ return self._print.get("spd_lvl")
108
+
109
+ @property
110
+ def print_error_code(self):
111
+ """Current print error code, or ``0`` if none."""
112
+ return self._print.get("print_error")
113
+
114
+ # ---- Temperatures ------------------------------------------------------
115
+
116
+ @property
117
+ def nozzle_temperature(self):
118
+ """Current nozzle temperature in degrees Celsius."""
119
+ return self._print.get("nozzle_temper")
120
+
121
+ @property
122
+ def nozzle_temperature_target(self):
123
+ """Target nozzle temperature in degrees Celsius."""
124
+ return self._print.get("nozzle_target_temper")
125
+
126
+ @property
127
+ def bed_temperature(self):
128
+ """Current bed temperature in degrees Celsius."""
129
+ return self._print.get("bed_temper")
130
+
131
+ @property
132
+ def bed_temperature_target(self):
133
+ """Target bed temperature in degrees Celsius."""
134
+ return self._print.get("bed_target_temper")
135
+
136
+ @property
137
+ def chamber_temperature(self):
138
+ """Current chamber temperature in degrees Celsius."""
139
+ return self._print.get("chamber_temper")
140
+
141
+ # ---- Fan speeds --------------------------------------------------------
142
+
143
+ @property
144
+ def part_fan_speed(self):
145
+ """Part cooling fan speed (0–255)."""
146
+ return self._print.get("cooling_fan_speed")
147
+
148
+ @property
149
+ def aux_fan_speed(self):
150
+ """Auxiliary fan speed (0–255)."""
151
+ return self._print.get("big_fan1_speed")
152
+
153
+ @property
154
+ def chamber_fan_speed(self):
155
+ """Chamber fan speed (0–255)."""
156
+ return self._print.get("big_fan2_speed")
157
+
158
+ # ---- Nozzle ------------------------------------------------------------
159
+
160
+ @property
161
+ def nozzle_type(self):
162
+ """Installed nozzle type string (e.g. ``"brass"``)."""
163
+ return self._print.get("nozzle_type")
164
+
165
+ @property
166
+ def nozzle_diameter(self):
167
+ """Installed nozzle diameter in mm."""
168
+ return self._print.get("nozzle_diameter")
169
+
170
+ # ---- Filament / AMS ----------------------------------------------------
171
+
172
+ @property
173
+ def ams_status(self):
174
+ """AMS filament / tray status dict, or ``None`` if no AMS present."""
175
+ return self._print.get("ams")
176
+
177
+ @property
178
+ def vt_tray(self):
179
+ """External spool (virtual tray) properties dict."""
180
+ return self._print.get("vt_tray")
181
+
182
+ # ---- System ------------------------------------------------------------
183
+
184
+ @property
185
+ def wifi_signal(self):
186
+ """WiFi signal strength string (e.g. ``"-45dBm"``)."""
187
+ return self._print.get("wifi_signal")
188
+
189
+ @property
190
+ def light_state(self):
191
+ """Chamber light state list from the system ``lights_report`` field."""
192
+ return self._system.get("lights_report")
193
+
194
+ # ---- Firmware ----------------------------------------------------------
195
+
196
+ @property
197
+ def firmware_version(self):
198
+ """Current OTA firmware version string, or ``None`` if not present."""
199
+ modules = self._info.get("module", [])
200
+ for module in modules:
201
+ if isinstance(module, dict) and module.get("name") == "ota":
202
+ return module.get("sw_ver")
203
+ return None
204
+
205
+
206
+ class BambuPrinter:
207
+ """Connect to a Bambu Labs printer over MQTT and query its status.
208
+
209
+ :param mqtt_client: A configured ``adafruit_minimqtt.MQTT`` instance.
210
+ :param serial_number: The printer's serial number, used to build MQTT topics.
211
+ :param response_timeout: Seconds to wait for a printer response (default 10).
212
+ """
213
+
214
+ def __init__(self, serial_number, response_timeout=10):
215
+ pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
216
+ ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
217
+
218
+ # Set up MQTT client
219
+ mqtt_client = MQTT.MQTT(
220
+ broker=BAMBU_BROKER,
221
+ port=8883,
222
+ username=USER_ID,
223
+ password=ACCESS_TOKEN,
224
+ socket_pool=pool,
225
+ ssl_context=ssl_context,
226
+ is_ssl=True,
227
+ )
228
+ self._mqtt = mqtt_client
229
+ self._serial = serial_number
230
+ self._response_timeout = response_timeout
231
+ self._report_topic = f"device/{serial_number}/report"
232
+ self._request_topic = f"device/{serial_number}/request"
233
+ self._last_response = None
234
+
235
+ self._mqtt.on_connect = self._on_connect
236
+ self._mqtt.on_message = self._on_message
237
+
238
+ # ---- Internal ----------------------------------------------------------
239
+
240
+ def _on_connect(self, client, _userdata, _flags, _rc):
241
+ client.subscribe(self._report_topic)
242
+
243
+ def _on_message(self, _client, _topic, message):
244
+ try:
245
+ data = json.loads(message)
246
+ except Exception:
247
+ return
248
+ self._last_response = data
249
+
250
+ def _send_and_wait(self, payload):
251
+ """Publish *payload* and block until the printer responds or timeout.
252
+
253
+ :returns: The raw JSON response dict, or ``None`` on timeout.
254
+ """
255
+ self._last_response = None
256
+ self._mqtt.publish(self._request_topic, json.dumps(payload))
257
+ deadline = time.monotonic() + self._response_timeout
258
+ while self._last_response is None and time.monotonic() < deadline:
259
+ self._mqtt.loop()
260
+ time.sleep(0.05)
261
+ return self._last_response
262
+
263
+ # ---- Connection --------------------------------------------------------
264
+
265
+ def connect(self):
266
+ """Establish the MQTT connection to the printer."""
267
+ print(f"Connecting to Bambu printer at {BAMBU_IP}...")
268
+ self._mqtt.connect()
269
+
270
+ def loop(self):
271
+ """Process pending MQTT messages. Call regularly in your main loop."""
272
+ self._mqtt.loop()
273
+
274
+ def loop_forever(self):
275
+ """Block and loop indefinitely, processing MQTT messages."""
276
+ self._mqtt.loop_forever()
277
+
278
+ def is_connected(self):
279
+ """Return ``True`` if the MQTT client is currently connected."""
280
+ return self._mqtt.is_connected()
281
+
282
+ # ---- Queries -----------------------------------------------------------
283
+
284
+ def pushall(self):
285
+ """Request a full state snapshot from the printer.
286
+
287
+ :returns: A :class:`PrinterStatus` instance, or ``None`` on timeout.
288
+ """
289
+ response = self._send_and_wait({"pushing": {"command": "pushall"}})
290
+ if response is not None:
291
+ return PrinterStatus(response)
292
+ return None
293
+
294
+ def get_version(self):
295
+ """Request hardware and firmware version information.
296
+
297
+ :returns: The raw JSON response dict, or ``None`` on timeout.
298
+ """
299
+ return self._send_and_wait({"info": {"sequence_id": "0", "command": "get_version"}})
300
+
301
+ def get_firmware_history(self):
302
+ """Request the firmware version history.
303
+
304
+ :returns: The raw JSON response dict, or ``None`` on timeout.
305
+ """
306
+ return self._send_and_wait({"upgrade": {"sequence_id": "0", "command": "get_history"}})
@@ -0,0 +1,222 @@
1
+ Metadata-Version: 2.4
2
+ Name: circuitpython-bambulabs
3
+ Version: 1.0.0
4
+ Summary: A library to interface with a Bambu Labs 3D printer and query status and information
5
+ Author-email: Paul Cutler <paul@paulcutler.org>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/prcutler/CircuitPython_bambulabs
8
+ Keywords: adafruit,blinka,circuitpython,micropython,bambulabs,mqtt,circuitpython,bambulabs
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Software Development :: Libraries
11
+ Classifier: Topic :: Software Development :: Embedded Systems
12
+ Classifier: Topic :: System :: Hardware
13
+ Classifier: Programming Language :: Python :: 3
14
+ Description-Content-Type: text/x-rst
15
+ License-File: LICENSE
16
+ Requires-Dist: Adafruit-Blinka
17
+ Requires-Dist: Adafruit_CircuitPython_MiniMQTT
18
+ Provides-Extra: optional
19
+ Dynamic: license-file
20
+
21
+ Introduction
22
+ ============
23
+
24
+
25
+ .. image:: https://readthedocs.org/projects/circuitpython-bambulabs/badge/?version=latest
26
+ :target: https://circuitpython-bambulabs.readthedocs.io/
27
+ :alt: Documentation Status
28
+
29
+
30
+
31
+ .. image:: https://img.shields.io/discord/327254708534116352.svg
32
+ :target: https://adafru.it/discord
33
+ :alt: Discord
34
+
35
+
36
+ .. image:: https://github.com/prcutler/CircuitPython_bambulabs/workflows/Build%20CI/badge.svg
37
+ :target: https://github.com/prcutler/CircuitPython_bambulabs/actions
38
+ :alt: Build Status
39
+
40
+
41
+ .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
42
+ :target: https://github.com/astral-sh/ruff
43
+ :alt: Code Style: Ruff
44
+
45
+ A library to interface with a Bambu Labs 3D printer and query status and information
46
+
47
+
48
+ Dependencies
49
+ =============
50
+ This driver depends on:
51
+
52
+ * `Adafruit CircuitPython <https://github.com/adafruit/circuitpython>`_
53
+ * `Adafruit_CircuitPython_MiniMQTT <https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT/>`_
54
+
55
+ Please ensure all dependencies are available on the CircuitPython filesystem.
56
+ This is easily achieved by downloading
57
+ `the Adafruit library and driver bundle <https://circuitpython.org/libraries>`_
58
+ or individual libraries can be installed using
59
+ `circup <https://github.com/adafruit/circup>`_.
60
+
61
+ Installing from PyPI
62
+ =====================
63
+ .. note:: This library is not available on PyPI yet. Install documentation is included
64
+ as a standard element. Stay tuned for PyPI availability!
65
+
66
+
67
+ On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from
68
+ PyPI <https://pypi.org/project/circuitpython-bambulabs/>`_.
69
+ To install for current user:
70
+
71
+ .. code-block:: shell
72
+
73
+ pip3 install circuitpython-bambulabs
74
+
75
+ To install system-wide (this may be required in some cases):
76
+
77
+ .. code-block:: shell
78
+
79
+ sudo pip3 install circuitpython-bambulabs
80
+
81
+ To install in a virtual environment in your current project:
82
+
83
+ .. code-block:: shell
84
+
85
+ mkdir project-name && cd project-name
86
+ python3 -m venv .venv
87
+ source .env/bin/activate
88
+ pip3 install circuitpython-bambulabs
89
+
90
+ Installing to a Connected CircuitPython Device with Circup
91
+ ==========================================================
92
+
93
+ Make sure that you have ``circup`` installed in your Python environment.
94
+ Install it with the following command if necessary:
95
+
96
+ .. code-block:: shell
97
+
98
+ pip3 install circup
99
+
100
+ With ``circup`` installed and your CircuitPython device connected use the
101
+ following command to install:
102
+
103
+ .. code-block:: shell
104
+
105
+ circup install bambulabs
106
+
107
+ Or the following command to update an existing version:
108
+
109
+ .. code-block:: shell
110
+
111
+ circup update
112
+
113
+ Obtaining Your Printer Credentials and Save in settings.toml
114
+ ============================================================
115
+
116
+ #. Login to `MakerWorld <https://makerworld.com/>`_.
117
+ #. Open the dev-tools (F12 in most browsers) and select
118
+ ``Application > Cookies > https://makerworld.com``.
119
+ #. Copy the ``token`` string and save it as ``BAMBU_ACCESS_TOKEN`` in ``settings.toml``.
120
+ #. Access `https://makerworld.com/api/v1/design-user-service/my/preference
121
+ <https://makerworld.com/api/v1/design-user-service/my/preference>`_ and copy
122
+ ``uid`` as ``USER_ID``.
123
+ #. Access `https://makerworld.com/api/v1/iot-service/api/user/bind
124
+ <https://makerworld.com/api/v1/iot-service/api/user/bind>`_ and copy
125
+ ``dev_id`` as ``DEVICE_ID``.
126
+ #. On your printer, go to Settings and obtain the IP address and copy it as
127
+ ``BAMBU_IP``.
128
+
129
+
130
+ You will need a ``settings.toml`` file with the following to connect via MQTT:
131
+
132
+ * CIRCUITPY_WIFI_SSID = "your_wifi_ssid"
133
+ * CIRCUITPY_WIFI_PASSWORD = "your_wifi_password"
134
+ * BAMBU_BROKER = os.getenv("BAMBU_BROKER")
135
+ * ACCESS_TOKEN = os.getenv("BAMBU_ACCESS_TOKEN")
136
+ * USER_ID = os.getenv("USER_ID")
137
+ * DEVICE_ID = "your_printer_serial_number"
138
+ * BAMBU_IP = "your_printer_IP_address"
139
+
140
+ Misc. information
141
+ =================
142
+
143
+ This library queries your Bambu Labs 3D printer via the Bambu Cloud using MQTT 5.0.
144
+ Local mode is not supported due to it using MQTT 3.1.1. Issuing commands to your
145
+ Bambu printer is out of scope for this library.
146
+
147
+ Usage Example
148
+ =============
149
+
150
+ .. code-block:: python
151
+
152
+ import json
153
+ import os
154
+
155
+ import wifi
156
+
157
+ import bambulabs as bl
158
+
159
+ # You will need a settings.toml file with the following to connect via MQTT:
160
+ # bambu_broker = os.getenv("BAMBU_BROKER")
161
+ # access_token = os.getenv("BAMBU_ACCESS_TOKEN")
162
+ # user_id = os.getenv("USER_ID")
163
+ # DEVICE_ID = "your_printer_serial_number"
164
+
165
+ # Set up networking
166
+ print("Connecting to AP...")
167
+ wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
168
+ print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}")
169
+ print(f"My IP address: {wifi.radio.ipv4_address}")
170
+
171
+ device_id = os.getenv("DEVICE_ID")
172
+
173
+ printer = bl.BambuPrinter(bl.BambuPrinter.__init__, device_id)
174
+ printer.connect()
175
+
176
+ status = printer.pushall()
177
+
178
+ if status is None:
179
+ print("Timed out waiting for pushall response.")
180
+ else:
181
+ # Print out each individual status
182
+ print("--- Printer Status ---")
183
+ print(f"State: {status.gcode_state}")
184
+ print(f"File: {status.gcode_file}")
185
+ print(f"Job: {status.subtask_name}")
186
+ print(f"Progress: {status.print_percentage}%")
187
+ print(f"Remaining: {status.remaining_time} min")
188
+ print(f"Layer: {status.current_layer} / {status.total_layers}")
189
+ print(f"Print speed: {status.print_speed}")
190
+ print(f"Print error: {status.print_error_code}")
191
+ print(f"Nozzle temp: {status.nozzle_temperature} / {status.nozzle_temperature_target} C")
192
+ print(f"Bed temp: {status.bed_temperature} / {status.bed_temperature_target} C")
193
+ print(f"Chamber temp: {status.chamber_temperature} C")
194
+ print(f"Part fan: {status.part_fan_speed}")
195
+ print(f"Aux fan: {status.aux_fan_speed}")
196
+ print(f"Chamber fan: {status.chamber_fan_speed}")
197
+ print(f"Nozzle type: {status.nozzle_type}")
198
+ print(f"Nozzle diameter: {status.nozzle_diameter} mm")
199
+ print(f"WiFi signal: {status.wifi_signal}")
200
+ print(f"Light state: {status.light_state}")
201
+ print(f"Firmware version: {status.firmware_version}")
202
+ print()
203
+ # Print the entire JSON response
204
+ print("--- Raw JSON ---")
205
+ print(json.dumps(status.raw))
206
+
207
+
208
+ Documentation
209
+ =============
210
+ API documentation for this library can be found on `Read the Docs <https://circuitpython-bambulabs.readthedocs.io/>`_.
211
+
212
+ For information on building library documentation, please check out
213
+ `this guide <https://learn.adafruit.com/creating-and-sharing-a-circuitpython-library/sharing-our-docs-on-readthedocs#sphinx-5-1>`_.
214
+
215
+ Contributing
216
+ ============
217
+
218
+ Contributions are welcome! Please read our `Code of Conduct
219
+ <https://github.com/prcutler/CircuitPython_bambulabs/blob/HEAD/CODE_OF_CONDUCT.md>`_
220
+ before contributing to help this project stay welcoming.
221
+
222
+ This library was created with the assistance of Claude. (I know, I know...)
@@ -0,0 +1,6 @@
1
+ bambulabs.py,sha256=g0kfTNGWUyHECH-tOj9TGPm1OwgKwHYQ0yhU_uDmle8,9928
2
+ circuitpython_bambulabs-1.0.0.dist-info/licenses/LICENSE,sha256=Maocfq0E-T2QKRxYKgtMbI4CSXO7GocYugv1-V3NJ7E,1078
3
+ circuitpython_bambulabs-1.0.0.dist-info/METADATA,sha256=JC09RhXCQCMoRhj526FrM-_v5alZNYR0s5aJXKhxDWc,8017
4
+ circuitpython_bambulabs-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ circuitpython_bambulabs-1.0.0.dist-info/top_level.txt,sha256=xZ8f7rtBtbUvYUfd7CMbLvyPgntgnNLcTUz4kQRRDQs,10
6
+ circuitpython_bambulabs-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Paul Cutler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ bambulabs