flashforge-python-api 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.
- flashforge/__init__.py +188 -0
- flashforge/api/__init__.py +17 -0
- flashforge/api/constants/__init__.py +10 -0
- flashforge/api/constants/commands.py +10 -0
- flashforge/api/constants/endpoints.py +11 -0
- flashforge/api/controls/__init__.py +16 -0
- flashforge/api/controls/control.py +357 -0
- flashforge/api/controls/files.py +171 -0
- flashforge/api/controls/info.py +292 -0
- flashforge/api/controls/job_control.py +561 -0
- flashforge/api/controls/temp_control.py +89 -0
- flashforge/api/filament/__init__.py +7 -0
- flashforge/api/filament/filament.py +39 -0
- flashforge/api/misc/__init__.py +8 -0
- flashforge/api/misc/scientific_notation.py +28 -0
- flashforge/api/misc/temperature.py +42 -0
- flashforge/api/network/__init__.py +10 -0
- flashforge/api/network/fnet_code.py +15 -0
- flashforge/api/network/utils.py +55 -0
- flashforge/client.py +381 -0
- flashforge/discovery/__init__.py +12 -0
- flashforge/discovery/discovery.py +388 -0
- flashforge/models/__init__.py +50 -0
- flashforge/models/machine_info.py +247 -0
- flashforge/models/responses.py +123 -0
- flashforge/tcp/__init__.py +41 -0
- flashforge/tcp/ff_client.py +587 -0
- flashforge/tcp/gcode/__init__.py +11 -0
- flashforge/tcp/gcode/gcode_controller.py +296 -0
- flashforge/tcp/gcode/gcodes.py +93 -0
- flashforge/tcp/parsers/__init__.py +27 -0
- flashforge/tcp/parsers/endstop_status.py +273 -0
- flashforge/tcp/parsers/location_info.py +68 -0
- flashforge/tcp/parsers/print_status.py +182 -0
- flashforge/tcp/parsers/printer_info.py +154 -0
- flashforge/tcp/parsers/temp_info.py +217 -0
- flashforge/tcp/parsers/thumbnail_info.py +242 -0
- flashforge/tcp/tcp_client.py +448 -0
- flashforge_python_api-1.0.0.dist-info/METADATA +123 -0
- flashforge_python_api-1.0.0.dist-info/RECORD +43 -0
- flashforge_python_api-1.0.0.dist-info/WHEEL +4 -0
- flashforge_python_api-1.0.0.dist-info/entry_points.txt +2 -0
- flashforge_python_api-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Location information parser for FlashForge 3D printers.
|
|
3
|
+
|
|
4
|
+
This module parses the response from M114 command to extract current
|
|
5
|
+
X, Y, Z coordinates of the printer's print head.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LocationInfo:
|
|
15
|
+
"""
|
|
16
|
+
Represents the current X, Y, and Z coordinates of the printer's print head.
|
|
17
|
+
|
|
18
|
+
This information is typically parsed from the response of an M114 G-code command,
|
|
19
|
+
which reports the current position.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
"""Initialize empty location info."""
|
|
24
|
+
self.x: str = ""
|
|
25
|
+
"""The current X-axis coordinate as a string (e.g., "10.00")."""
|
|
26
|
+
|
|
27
|
+
self.y: str = ""
|
|
28
|
+
"""The current Y-axis coordinate as a string (e.g., "20.50")."""
|
|
29
|
+
|
|
30
|
+
self.z: str = ""
|
|
31
|
+
"""The current Z-axis coordinate as a string (e.g., "5.25")."""
|
|
32
|
+
|
|
33
|
+
def from_replay(self, replay: str) -> Optional['LocationInfo']:
|
|
34
|
+
"""
|
|
35
|
+
Parse a raw string replay from M114 command to populate coordinate info.
|
|
36
|
+
|
|
37
|
+
The parsing logic assumes the replay is a multi-line string where the second line
|
|
38
|
+
(data[1]) contains the coordinate data in a format like "X:10.00 Y:20.50 Z:5.25 ...".
|
|
39
|
+
It splits this line by spaces and then extracts the values for X, Y, and Z by
|
|
40
|
+
removing the prefixes "X:", "Y:", and "Z:".
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
replay: The raw multi-line string response from the printer
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The populated LocationInfo instance, or None if parsing fails
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
data = replay.split('\n')
|
|
50
|
+
# The first line (data[0]) is often the command echo (e.g., "ok M114") or similar,
|
|
51
|
+
# actual coordinate data is expected on the second line.
|
|
52
|
+
loc_data = data[1].split(' ')
|
|
53
|
+
self.x = loc_data[0].replace("X:", "").strip()
|
|
54
|
+
self.y = loc_data[1].replace("Y:", "").strip()
|
|
55
|
+
self.z = loc_data[2].replace("Z:", "").strip()
|
|
56
|
+
return self
|
|
57
|
+
except Exception:
|
|
58
|
+
logger.error("LocationInfo replay has bad/null data")
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def __str__(self) -> str:
|
|
62
|
+
"""
|
|
63
|
+
Return a string representation of the location information.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
A string in the format "X: [X_value] Y: [Y_value] Z: [Z_value]"
|
|
67
|
+
"""
|
|
68
|
+
return f"X: {self.x} Y: {self.y} Z: {self.z}"
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FlashForge Python API - Print Status Parser
|
|
3
|
+
|
|
4
|
+
Parses print progress information from M27 command responses.
|
|
5
|
+
"""
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PrintStatus:
|
|
10
|
+
"""
|
|
11
|
+
Represents the status of an ongoing print job, including SD card byte progress and layer progress.
|
|
12
|
+
|
|
13
|
+
This information is typically parsed from the response of an M27 G-code command,
|
|
14
|
+
which reports the print progress from the SD card.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
"""Initialize a new PrintStatus instance."""
|
|
19
|
+
self.sd_current: str = ""
|
|
20
|
+
self.sd_total: str = ""
|
|
21
|
+
self.layer_current: str = ""
|
|
22
|
+
self.layer_total: str = ""
|
|
23
|
+
|
|
24
|
+
def from_replay(self, replay: str) -> Optional['PrintStatus']:
|
|
25
|
+
"""
|
|
26
|
+
Parses a raw string replay (typically from an M27 command) to populate
|
|
27
|
+
the print status properties of this instance.
|
|
28
|
+
|
|
29
|
+
The parsing logic expects a multi-line string:
|
|
30
|
+
- Line 1 (data[0]): Usually a command echo, ignored
|
|
31
|
+
- Line 2 (data[1]): Contains SD card progress, e.g., "SD printing byte 12345/67890"
|
|
32
|
+
It extracts the current and total bytes
|
|
33
|
+
- Line 3 (data[2]): Contains layer progress, e.g., "Layer: 10/250"
|
|
34
|
+
It extracts the current and total layers
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
replay: The raw multi-line string response from the printer
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
The populated PrintStatus instance, or None if parsing fails
|
|
41
|
+
"""
|
|
42
|
+
if not replay:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
data = replay.split('\n')
|
|
47
|
+
|
|
48
|
+
# Parse SD progress (line 1)
|
|
49
|
+
if len(data) > 1:
|
|
50
|
+
# Example: "SD printing byte 12345/67890"
|
|
51
|
+
sd_progress = data[1].replace("SD printing byte ", "").strip()
|
|
52
|
+
sd_progress_data = sd_progress.split('/')
|
|
53
|
+
|
|
54
|
+
if len(sd_progress_data) >= 2:
|
|
55
|
+
self.sd_current = sd_progress_data[0].strip()
|
|
56
|
+
self.sd_total = sd_progress_data[1].strip()
|
|
57
|
+
else:
|
|
58
|
+
print("PrintStatus: Invalid SD progress format")
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
# Parse layer progress (line 2)
|
|
62
|
+
if len(data) > 2:
|
|
63
|
+
try:
|
|
64
|
+
# Example: "Layer: 10/250"
|
|
65
|
+
layer_progress = data[2].replace("Layer: ", "").strip()
|
|
66
|
+
except Exception:
|
|
67
|
+
print("PrintStatus: Bad layer progress")
|
|
68
|
+
print(f"Raw printer replay: {replay}")
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
lp_data = layer_progress.split('/')
|
|
73
|
+
if len(lp_data) >= 2:
|
|
74
|
+
self.layer_current = lp_data[0].strip()
|
|
75
|
+
self.layer_total = lp_data[1].strip()
|
|
76
|
+
else:
|
|
77
|
+
print("PrintStatus: Invalid layer progress format")
|
|
78
|
+
print(f"layerProgress: {layer_progress}")
|
|
79
|
+
return None
|
|
80
|
+
except Exception:
|
|
81
|
+
print("PrintStatus: Bad layer progress parsing")
|
|
82
|
+
print(f"layerProgress: {layer_progress}")
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f"Error parsing print status: {e}")
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def get_print_percent(self) -> float:
|
|
92
|
+
"""
|
|
93
|
+
Calculates the print progress percentage based on the current and total layers.
|
|
94
|
+
The result is clamped between 0 and 100.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The print progress percentage (0-100), rounded to the nearest integer.
|
|
98
|
+
Returns NaN if layer information is not available or invalid.
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
current_layer = int(self.layer_current)
|
|
102
|
+
total_layers = int(self.layer_total)
|
|
103
|
+
|
|
104
|
+
if total_layers == 0:
|
|
105
|
+
return float('nan')
|
|
106
|
+
|
|
107
|
+
perc = (current_layer / total_layers) * 100
|
|
108
|
+
return round(min(100, max(0, perc))) # Clamp between 0 and 100
|
|
109
|
+
|
|
110
|
+
except (ValueError, TypeError):
|
|
111
|
+
return float('nan')
|
|
112
|
+
|
|
113
|
+
def get_layer_progress(self) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Gets the layer progress as a string.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
A string in the format "currentLayer/totalLayers"
|
|
119
|
+
"""
|
|
120
|
+
return f"{self.layer_current}/{self.layer_total}"
|
|
121
|
+
|
|
122
|
+
def get_sd_progress(self) -> str:
|
|
123
|
+
"""
|
|
124
|
+
Gets the SD card byte progress as a string.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
A string in the format "currentBytes/totalBytes"
|
|
128
|
+
"""
|
|
129
|
+
return f"{self.sd_current}/{self.sd_total}"
|
|
130
|
+
|
|
131
|
+
def get_sd_percent(self) -> float:
|
|
132
|
+
"""
|
|
133
|
+
Calculates the SD card progress percentage based on current and total bytes.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
The SD progress percentage (0-100), or NaN if data is invalid
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
current_bytes = int(self.sd_current)
|
|
140
|
+
total_bytes = int(self.sd_total)
|
|
141
|
+
|
|
142
|
+
if total_bytes == 0:
|
|
143
|
+
return float('nan')
|
|
144
|
+
|
|
145
|
+
perc = (current_bytes / total_bytes) * 100
|
|
146
|
+
return round(min(100, max(0, perc))) # Clamp between 0 and 100
|
|
147
|
+
|
|
148
|
+
except (ValueError, TypeError):
|
|
149
|
+
return float('nan')
|
|
150
|
+
|
|
151
|
+
def is_complete(self) -> bool:
|
|
152
|
+
"""
|
|
153
|
+
Checks if the print is complete based on layer progress.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
True if current layer equals total layers, False otherwise
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
current = int(self.layer_current)
|
|
160
|
+
total = int(self.layer_total)
|
|
161
|
+
return current >= total and total > 0
|
|
162
|
+
except (ValueError, TypeError):
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
def __str__(self) -> str:
|
|
166
|
+
"""String representation of the print status."""
|
|
167
|
+
layer_perc = self.get_print_percent()
|
|
168
|
+
sd_perc = self.get_sd_percent()
|
|
169
|
+
|
|
170
|
+
layer_str = "nan%" if layer_perc != layer_perc else f"{layer_perc}%" # Check for NaN
|
|
171
|
+
sd_str = "nan%" if sd_perc != sd_perc else f"{sd_perc}%" # Check for NaN
|
|
172
|
+
|
|
173
|
+
return (f"PrintStatus(layer={self.get_layer_progress()} [{layer_str}], "
|
|
174
|
+
f"sd={self.get_sd_progress()} [{sd_str}])")
|
|
175
|
+
|
|
176
|
+
def __repr__(self) -> str:
|
|
177
|
+
"""Detailed string representation for debugging."""
|
|
178
|
+
return (f"PrintStatus("
|
|
179
|
+
f"sd_current='{self.sd_current}', "
|
|
180
|
+
f"sd_total='{self.sd_total}', "
|
|
181
|
+
f"layer_current='{self.layer_current}', "
|
|
182
|
+
f"layer_total='{self.layer_total}')")
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Printer information parser for FlashForge 3D printers.
|
|
3
|
+
|
|
4
|
+
This module parses the response from M115 command to extract printer details
|
|
5
|
+
like model, firmware version, serial number, and capabilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PrinterInfo:
|
|
15
|
+
"""
|
|
16
|
+
Represents general information about the FlashForge 3D printer.
|
|
17
|
+
|
|
18
|
+
This information is typically parsed from the response of an M115 G-code command,
|
|
19
|
+
which provides details about the printer's firmware and capabilities.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
"""Initialize empty printer info."""
|
|
24
|
+
self.type_name: str = ""
|
|
25
|
+
"""The machine type or model name (e.g., "FlashForge Adventurer 5M Pro")."""
|
|
26
|
+
|
|
27
|
+
self.name: str = ""
|
|
28
|
+
"""The user-assigned name of the printer."""
|
|
29
|
+
|
|
30
|
+
self.firmware_version: str = ""
|
|
31
|
+
"""The firmware version currently installed on the printer."""
|
|
32
|
+
|
|
33
|
+
self.serial_number: str = ""
|
|
34
|
+
"""The unique serial number of the printer."""
|
|
35
|
+
|
|
36
|
+
self.dimensions: str = ""
|
|
37
|
+
"""The build dimensions of the printer (e.g., "X:220 Y:220 Z:220")."""
|
|
38
|
+
|
|
39
|
+
self.mac_address: str = ""
|
|
40
|
+
"""The MAC address of the printer's network interface."""
|
|
41
|
+
|
|
42
|
+
self.tool_count: str = ""
|
|
43
|
+
"""The number of tools (extruders) the printer has."""
|
|
44
|
+
|
|
45
|
+
def from_replay(self, replay: str) -> Optional['PrinterInfo']:
|
|
46
|
+
"""
|
|
47
|
+
Parse a raw string replay from M115 command to populate printer info.
|
|
48
|
+
|
|
49
|
+
The M115 response is expected to be a multi-line string where each line
|
|
50
|
+
provides a piece of information in a "Key: Value" format.
|
|
51
|
+
|
|
52
|
+
Expected format:
|
|
53
|
+
- Line 1: Command echo/header (ignored)
|
|
54
|
+
- Line 2: "Machine Type: [TypeName]"
|
|
55
|
+
- Line 3: "Machine Name: [Name]"
|
|
56
|
+
- Line 4: "Firmware: [FirmwareVersion]"
|
|
57
|
+
- Line 5: "SN: [SerialNumber]"
|
|
58
|
+
- Line 6: Dimensions string (e.g., "X:220 Y:220 Z:220")
|
|
59
|
+
- Line 7: "Tool count: [ToolCount]"
|
|
60
|
+
- Line 8: "Mac Address:[MacAddress]"
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
replay: The raw multi-line string response from the M115 command
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The populated PrinterInfo instance, or None if parsing fails
|
|
67
|
+
"""
|
|
68
|
+
if not replay:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
data = replay.split('\n')
|
|
73
|
+
|
|
74
|
+
# Parse machine type (line 2)
|
|
75
|
+
name = self._get_right(data[1]) # Expected: "Machine Type: Adventurer 5M Pro"
|
|
76
|
+
if name is None:
|
|
77
|
+
logger.error("PrinterInfo replay has null Machine Type")
|
|
78
|
+
return None
|
|
79
|
+
self.type_name = name
|
|
80
|
+
|
|
81
|
+
# Parse machine name (line 3)
|
|
82
|
+
nick = self._get_right(data[2]) # Expected: "Machine Name: MyPrinter"
|
|
83
|
+
if nick is None:
|
|
84
|
+
logger.error("PrinterInfo replay has null Machine Name")
|
|
85
|
+
return None
|
|
86
|
+
self.name = nick
|
|
87
|
+
|
|
88
|
+
# Parse firmware version (line 4)
|
|
89
|
+
fw = self._get_right(data[3]) # Expected: "Firmware: V1.2.3"
|
|
90
|
+
if fw is None:
|
|
91
|
+
logger.error("PrinterInfo replay has null firmware version")
|
|
92
|
+
return None
|
|
93
|
+
self.firmware_version = fw
|
|
94
|
+
|
|
95
|
+
# Parse serial number (line 5)
|
|
96
|
+
sn = self._get_right(data[4]) # Expected: "SN: SN12345"
|
|
97
|
+
if sn is None:
|
|
98
|
+
logger.error("PrinterInfo replay has null serial number")
|
|
99
|
+
return None
|
|
100
|
+
self.serial_number = sn
|
|
101
|
+
|
|
102
|
+
# Parse dimensions (line 6) - direct string
|
|
103
|
+
if len(data) > 5:
|
|
104
|
+
self.dimensions = data[5].strip() # Expected: "X:220 Y:220 Z:220"
|
|
105
|
+
|
|
106
|
+
# Parse tool count (line 7)
|
|
107
|
+
if len(data) > 6:
|
|
108
|
+
tcs = self._get_right(data[6]) # Expected: "Tool count: 1"
|
|
109
|
+
if tcs is None:
|
|
110
|
+
logger.error("PrinterInfo replay has null tool count")
|
|
111
|
+
return None
|
|
112
|
+
self.tool_count = tcs
|
|
113
|
+
|
|
114
|
+
# Parse MAC address (line 8)
|
|
115
|
+
if len(data) > 7:
|
|
116
|
+
self.mac_address = data[7].replace("Mac Address:", "").strip()
|
|
117
|
+
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Error creating PrinterInfo instance from replay: {e}")
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
def _get_right(self, rp_data: str) -> Optional[str]:
|
|
125
|
+
"""
|
|
126
|
+
Helper function to extract the value part of a "Key: Value" string.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
rp_data: The input string (e.g., "Machine Type: Adventurer 5M Pro")
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The extracted value string (e.g., "Adventurer 5M Pro"), or None if parsing fails
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
return rp_data.split(':', 1)[1].strip()
|
|
136
|
+
except (IndexError, AttributeError):
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
def __str__(self) -> str:
|
|
140
|
+
"""
|
|
141
|
+
Return a string representation of the printer information.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
A multi-line string detailing the printer's properties
|
|
145
|
+
"""
|
|
146
|
+
return (
|
|
147
|
+
f"Printer Type: {self.type_name}\n"
|
|
148
|
+
f"Name: {self.name}\n"
|
|
149
|
+
f"Firmware: {self.firmware_version}\n"
|
|
150
|
+
f"Serial Number: {self.serial_number}\n"
|
|
151
|
+
f"Print Dimensions: {self.dimensions}\n"
|
|
152
|
+
f"Tool Count: {self.tool_count}\n"
|
|
153
|
+
f"MAC Address: {self.mac_address}"
|
|
154
|
+
)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Temperature information parser for FlashForge 3D printers.
|
|
3
|
+
|
|
4
|
+
This module parses the response from M105 command to extract temperature data
|
|
5
|
+
for the extruder and print bed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TempData:
|
|
15
|
+
"""
|
|
16
|
+
Represents temperature data for a single component (e.g., extruder or bed).
|
|
17
|
+
|
|
18
|
+
Includes current temperature and target (set) temperature.
|
|
19
|
+
Temperatures are stored as strings but can be retrieved as numbers.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, data: str) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Create a TempData instance by parsing a temperature string.
|
|
25
|
+
|
|
26
|
+
The input string can be in the format "current/set" (e.g., "210/210")
|
|
27
|
+
or just "current" (e.g., "25") if the target temperature is not specified.
|
|
28
|
+
It also handles and removes a trailing "/0.0" if present from some printer firmwares.
|
|
29
|
+
All temperatures are rounded to the nearest integer.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
data: The temperature data string (e.g., "210/210", "25", "60/60/0.0")
|
|
33
|
+
"""
|
|
34
|
+
# Handle potential formatting issues by removing any non-relevant part
|
|
35
|
+
data = data.replace('/0.0', '') # Remove trailing '/0.0' if exists
|
|
36
|
+
|
|
37
|
+
if "/" in data:
|
|
38
|
+
# Replay has current/set temps
|
|
39
|
+
split_temps = data.split('/')
|
|
40
|
+
self._current = self._parse_temp_data(split_temps[0].strip())
|
|
41
|
+
self._set = self._parse_temp_data(split_temps[1].strip()) if len(split_temps) > 1 else None
|
|
42
|
+
else:
|
|
43
|
+
# Replay only has current temp (when printer is idle)
|
|
44
|
+
self._current = self._parse_temp_data(data)
|
|
45
|
+
self._set = None
|
|
46
|
+
|
|
47
|
+
def _parse_temp_data(self, data: str) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Parse a raw temperature string value, round it, and return as string.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
data: The raw temperature string (e.g., "210.5", "60")
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The rounded temperature as a string
|
|
56
|
+
"""
|
|
57
|
+
if "." in data:
|
|
58
|
+
data = data.split('.')[0].strip() # Truncate decimal part before rounding
|
|
59
|
+
temp = round(float(data))
|
|
60
|
+
return str(temp)
|
|
61
|
+
|
|
62
|
+
def get_full(self) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Get the full temperature string, including current and set temperatures.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
A string in the format "current/set" or just "current" if set temperature is not available
|
|
68
|
+
"""
|
|
69
|
+
if self._set is None:
|
|
70
|
+
return self._current
|
|
71
|
+
return f"{self._current}/{self._set}"
|
|
72
|
+
|
|
73
|
+
def get_current(self) -> int:
|
|
74
|
+
"""
|
|
75
|
+
Get the current temperature as a number.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
The current temperature in Celsius
|
|
79
|
+
"""
|
|
80
|
+
return int(self._current)
|
|
81
|
+
|
|
82
|
+
def get_set(self) -> int:
|
|
83
|
+
"""
|
|
84
|
+
Get the target (set) temperature as a number.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
The set temperature in Celsius, or 0 if not set
|
|
88
|
+
"""
|
|
89
|
+
return int(self._set) if self._set else 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TempInfo:
|
|
93
|
+
"""
|
|
94
|
+
Represents the temperature information for the printer's extruder and bed.
|
|
95
|
+
|
|
96
|
+
This data is typically parsed from the response of an M105 G-code command,
|
|
97
|
+
which reports the current and target temperatures.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(self) -> None:
|
|
101
|
+
"""Initialize empty temperature info."""
|
|
102
|
+
self._extruder_temp: Optional[TempData] = None
|
|
103
|
+
"""Temperature data for the extruder."""
|
|
104
|
+
|
|
105
|
+
self._bed_temp: Optional[TempData] = None
|
|
106
|
+
"""Temperature data for the print bed."""
|
|
107
|
+
|
|
108
|
+
def from_replay(self, replay: str) -> Optional['TempInfo']:
|
|
109
|
+
"""
|
|
110
|
+
Parse a raw string replay from M105 command to populate temperature info.
|
|
111
|
+
|
|
112
|
+
The M105 response format is usually a single line (after the "ok" or command echo)
|
|
113
|
+
containing temperature segments like "T0:25/0" or "T:210/210 B:60/60".
|
|
114
|
+
This method splits the relevant line by spaces and then parses each segment.
|
|
115
|
+
It looks for segments starting with "T0:", "T):", or "T:" for extruder temperature,
|
|
116
|
+
and "B:" for bed temperature.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
replay: The raw multi-line string response from the printer
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
The populated TempInfo instance, or None if parsing fails
|
|
123
|
+
"""
|
|
124
|
+
if not replay:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
data = replay.split('\n')
|
|
129
|
+
if len(data) <= 1:
|
|
130
|
+
logger.error(f"TempInfo replay has invalid data: {data}")
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
# Relevant temperature data is usually on the second line (data[1])
|
|
134
|
+
# e.g., "T0:25/0 B:28/0 @:0 B@:0" or "T:210/210 B:60/60"
|
|
135
|
+
temp_data = data[1].split(' ')
|
|
136
|
+
extruder_data_str = None
|
|
137
|
+
bed_data_str = None
|
|
138
|
+
|
|
139
|
+
# Parse each temperature segment
|
|
140
|
+
for segment in temp_data:
|
|
141
|
+
# Check for extruder temperature (T0, T, or T) for some printers)
|
|
142
|
+
if segment.startswith('T0:'):
|
|
143
|
+
extruder_data_str = segment.replace('T0:', '')
|
|
144
|
+
elif segment.startswith('T):'): # Some printers might use T):
|
|
145
|
+
extruder_data_str = segment.replace('T):', '')
|
|
146
|
+
elif segment.startswith('T:'): # General case for T:
|
|
147
|
+
extruder_data_str = segment.replace('T:', '')
|
|
148
|
+
# Check for bed temperature
|
|
149
|
+
elif segment.startswith('B:'):
|
|
150
|
+
bed_data_str = segment.replace('B:', '')
|
|
151
|
+
|
|
152
|
+
# If we found extruder data, create TempData object
|
|
153
|
+
if extruder_data_str:
|
|
154
|
+
self._extruder_temp = TempData(extruder_data_str)
|
|
155
|
+
else:
|
|
156
|
+
logger.error(f"No extruder temperature found in replay data: {replay}")
|
|
157
|
+
return None # Extruder temp is critical
|
|
158
|
+
|
|
159
|
+
# If we found bed data, create TempData object; otherwise, default to 0/0
|
|
160
|
+
if bed_data_str:
|
|
161
|
+
self._bed_temp = TempData(bed_data_str)
|
|
162
|
+
else:
|
|
163
|
+
logger.warning(f"No bed temperature found in replay data, defaulting to 0/0: {replay}")
|
|
164
|
+
self._bed_temp = TempData('0/0') # Default if not present
|
|
165
|
+
|
|
166
|
+
return self
|
|
167
|
+
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.error(f"Unable to create TempInfo instance from replay: {e}")
|
|
170
|
+
logger.error(f"Raw replay data: {replay}")
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
def get_extruder_temp(self) -> Optional[TempData]:
|
|
174
|
+
"""
|
|
175
|
+
Get the extruder temperature data.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
A TempData object for the extruder, or None if not available
|
|
179
|
+
"""
|
|
180
|
+
return self._extruder_temp
|
|
181
|
+
|
|
182
|
+
def get_bed_temp(self) -> Optional[TempData]:
|
|
183
|
+
"""
|
|
184
|
+
Get the print bed temperature data.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
A TempData object for the bed, or None if not available
|
|
188
|
+
"""
|
|
189
|
+
return self._bed_temp
|
|
190
|
+
|
|
191
|
+
def is_cooled(self) -> bool:
|
|
192
|
+
"""
|
|
193
|
+
Check if both the bed and extruder are cooled down to relatively low temperatures.
|
|
194
|
+
|
|
195
|
+
Bed temperature <= 40°C and extruder temperature <= 200°C (though 200 is still hot).
|
|
196
|
+
Use with caution, as "cooled" here is relative and 200C is still very hot for an extruder.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
True if temperatures are at or below the defined thresholds, False otherwise
|
|
200
|
+
"""
|
|
201
|
+
bed_temp = self._bed_temp.get_current() if self._bed_temp else 0
|
|
202
|
+
extruder_temp = self._extruder_temp.get_current() if self._extruder_temp else 0
|
|
203
|
+
return bed_temp <= 40 and extruder_temp <= 200
|
|
204
|
+
|
|
205
|
+
def are_temps_safe(self) -> bool:
|
|
206
|
+
"""
|
|
207
|
+
Check if the current temperatures are within a generally safe operating range.
|
|
208
|
+
|
|
209
|
+
Prevents overheating (extruder < 250°C, bed < 100°C).
|
|
210
|
+
These are arbitrary "safe" limits and might need adjustment based on specific printer/material.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
True if temperatures are below the defined "safe" thresholds, False otherwise
|
|
214
|
+
"""
|
|
215
|
+
bed_temp = self._bed_temp.get_current() if self._bed_temp else 0
|
|
216
|
+
extruder_temp = self._extruder_temp.get_current() if self._extruder_temp else 0
|
|
217
|
+
return extruder_temp < 250 and bed_temp < 100
|